feat(dashboard): transform home dashboard into full viewport video overlay with transparent quick launch modules bar
This commit is contained in:
@@ -1,17 +1,85 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Search, Bell, Menu, Sparkles, User, Calendar, ShieldCheck, LogIn } from 'lucide-react';
|
||||
import {
|
||||
Search,
|
||||
Bell,
|
||||
Menu,
|
||||
Calendar,
|
||||
LogIn,
|
||||
Check,
|
||||
X,
|
||||
BookOpen,
|
||||
Bus,
|
||||
Users,
|
||||
Calendar as ClubIcon,
|
||||
Code2,
|
||||
Trophy,
|
||||
Bot,
|
||||
Rocket,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { BUS_ROUTES, FACULTY_DATA, CLUBS_DATA } from '@/constants';
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar: () => void;
|
||||
}
|
||||
|
||||
interface NotificationItem {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
type: 'academic' | 'dev' | 'campus' | 'bus';
|
||||
}
|
||||
|
||||
export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [isNotifOpen, setIsNotifOpen] = useState(false);
|
||||
const [currentDate, setCurrentDate] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle } = useAuth();
|
||||
const notifRef = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Moodle Notes & PYQs Updated',
|
||||
message: '1st to 8th Semester notes and previous year question papers are published on Moodle.',
|
||||
time: '10m ago',
|
||||
read: false,
|
||||
type: 'academic',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Dev Collab Hub Open',
|
||||
message: 'Post open-source project ideas or find co-developers across departments.',
|
||||
time: '1h ago',
|
||||
read: false,
|
||||
type: 'dev',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'LeetCode Campus Rankings Live',
|
||||
message: 'Monthly competitive programming leaderboard has been updated.',
|
||||
time: '3h ago',
|
||||
read: false,
|
||||
type: 'academic',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Campus Bus Schedule Confirmed',
|
||||
message: 'All 4 campus bus routes operating on 7:00 AM morning schedule.',
|
||||
time: '1d ago',
|
||||
read: true,
|
||||
type: 'bus',
|
||||
},
|
||||
]);
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
@@ -24,33 +92,95 @@ export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
setCurrentDate(formatted);
|
||||
}, []);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!searchQuery.trim()) return;
|
||||
// Listen for ⌘K shortcut
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
setIsSearchOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
// Close dropdowns on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (notifRef.current && !notifRef.current.contains(e.target as Node)) {
|
||||
setIsNotifOpen(false);
|
||||
}
|
||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
||||
setIsSearchOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const markAllAsRead = () => {
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||
};
|
||||
|
||||
const clearNotifications = () => {
|
||||
setNotifications([]);
|
||||
};
|
||||
|
||||
// Search indexing & results calculation
|
||||
const searchResults = (() => {
|
||||
if (!searchQuery.trim()) return [];
|
||||
const q = searchQuery.toLowerCase();
|
||||
if (q.includes('note') || q.includes('pyq') || q.includes('syllabus')) {
|
||||
navigate('/notes');
|
||||
} else if (q.includes('bus') || q.includes('route')) {
|
||||
navigate('/bus-routes');
|
||||
} else if (q.includes('faculty') || q.includes('teacher') || q.includes('prof')) {
|
||||
navigate('/faculty');
|
||||
} else if (q.includes('club') || q.includes('event')) {
|
||||
navigate('/events');
|
||||
} else if (q.includes('ai') || q.includes('chat') || q.includes('bot')) {
|
||||
navigate('/ai-assistant');
|
||||
} else if (q.includes('community') || q.includes('qa') || q.includes('question')) {
|
||||
navigate('/community');
|
||||
} else if (q.includes('collab') || q.includes('dev')) {
|
||||
navigate('/collab');
|
||||
} else if (q.includes('raise') || q.includes('incubator') || q.includes('pitch')) {
|
||||
navigate('/raise');
|
||||
} else {
|
||||
navigate(`/notes?search=${encodeURIComponent(searchQuery)}`);
|
||||
const results: { title: string; desc: string; path: string; icon: any; category: string }[] = [];
|
||||
|
||||
// Check modules
|
||||
if ('notes pyqs moodle syllabus question paper'.includes(q)) {
|
||||
results.push({ title: 'Academic Notes & PYQs', desc: 'Semester course notes & past papers', path: '/notes', icon: BookOpen, category: 'Academics' });
|
||||
}
|
||||
if ('bus route timing transport travel pickup'.includes(q)) {
|
||||
results.push({ title: 'Campus Bus Routes', desc: 'Schedules and stops for all campus buses', path: '/bus-routes', icon: Bus, category: 'Utilities' });
|
||||
}
|
||||
if ('chatbot assistant question help contact rules'.includes(q)) {
|
||||
results.push({ title: 'Campus Chatbot', desc: 'Instant Q&A about campus life & rules', path: '/ai-assistant', icon: Bot, category: 'Academics' });
|
||||
}
|
||||
if ('collab dev project developer team coding github'.includes(q)) {
|
||||
results.push({ title: 'Dev Collab Hub', desc: 'Find co-developers & build projects', path: '/collab', icon: Code2, category: 'Dev' });
|
||||
}
|
||||
if ('leetcode ranking leaderboard coding rank'.includes(q)) {
|
||||
results.push({ title: 'LeetCode Leaderboard', desc: 'Campus competitive coding rankings', path: '/leetcode', icon: Trophy, category: 'Dev' });
|
||||
}
|
||||
|
||||
// Search Bus Routes
|
||||
BUS_ROUTES.forEach((r) => {
|
||||
if (r.name.toLowerCase().includes(q) || r.number.toLowerCase().includes(q) || r.from.toLowerCase().includes(q)) {
|
||||
results.push({ title: `${r.number}: ${r.name}`, desc: `Departure: ${r.departureTime} from ${r.from}`, path: '/bus-routes', icon: Bus, category: 'Bus Route' });
|
||||
}
|
||||
});
|
||||
|
||||
// Search Faculty
|
||||
FACULTY_DATA.forEach((f) => {
|
||||
if (f.name.toLowerCase().includes(q) || f.department.toLowerCase().includes(q) || (f.specialization && f.specialization.toLowerCase().includes(q))) {
|
||||
results.push({ title: f.name, desc: `${f.designation} — ${f.department}`, path: '/faculty', icon: Users, category: 'Faculty' });
|
||||
}
|
||||
});
|
||||
|
||||
// Search Clubs
|
||||
CLUBS_DATA.forEach((c) => {
|
||||
if (c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q)) {
|
||||
results.push({ title: c.name, desc: c.description, path: '/events', icon: ClubIcon, category: 'Club' });
|
||||
}
|
||||
});
|
||||
|
||||
return results.slice(0, 6);
|
||||
})();
|
||||
|
||||
const handleSelectResult = (path: string) => {
|
||||
setIsSearchOpen(false);
|
||||
setSearchQuery('');
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 w-full bg-white/80 backdrop-blur-md border-b border-slate-200/80 px-4 lg:px-8 py-3 flex items-center justify-between shadow-xs">
|
||||
<header className="sticky top-0 z-30 w-full bg-white/90 backdrop-blur-md border-b border-slate-200/80 px-4 lg:px-8 py-3 flex items-center justify-between shadow-xs">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Mobile Toggle */}
|
||||
<button
|
||||
@@ -61,39 +191,190 @@ export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Global Search Bar */}
|
||||
<form onSubmit={handleSearch} className="relative w-64 sm:w-80 md:w-96">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search notes, faculty, bus routes, clubs..."
|
||||
className="w-full pl-10 pr-12 py-2 text-xs font-medium bg-slate-100/90 hover:bg-slate-100 focus:bg-white border border-transparent focus:border-purple-500/40 rounded-xl text-slate-800 placeholder-slate-400 outline-none transition-all shadow-inner"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
<span className="hidden sm:inline-flex absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-semibold text-slate-400 bg-white border border-slate-200 px-1.5 py-0.5 rounded-md shadow-2xs">
|
||||
⌘K
|
||||
</span>
|
||||
</form>
|
||||
{/* Global Interactive Search Input Container */}
|
||||
<div className="relative" ref={searchRef}>
|
||||
<div
|
||||
onClick={() => setIsSearchOpen(true)}
|
||||
className="relative w-64 sm:w-80 md:w-96 cursor-text"
|
||||
>
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setIsSearchOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsSearchOpen(true)}
|
||||
placeholder="Search notes, faculty, bus routes, clubs..."
|
||||
className="w-full pl-10 pr-12 py-2 text-xs font-medium bg-slate-100/90 hover:bg-slate-100 focus:bg-white border border-transparent focus:border-indigo-500/40 rounded-xl text-slate-800 placeholder-slate-400 outline-none transition-all shadow-inner"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
<span className="hidden sm:inline-flex absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-semibold text-slate-400 bg-white border border-slate-200 px-1.5 py-0.5 rounded-md shadow-2xs">
|
||||
⌘K
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Search Live Results Dropdown Overlay */}
|
||||
{isSearchOpen && (
|
||||
<div className="absolute top-full left-0 mt-2 w-full sm:w-[420px] bg-white rounded-2xl border border-slate-200 shadow-xl overflow-hidden z-50 p-2 text-left">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-slate-100">
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">
|
||||
{searchQuery.trim() ? 'Search Results' : 'Quick Navigation'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsSearchOpen(false)}
|
||||
className="p-1 rounded-md text-slate-400 hover:text-slate-600 hover:bg-slate-100"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-72 overflow-y-auto custom-scrollbar p-1 space-y-1">
|
||||
{searchResults.length > 0 ? (
|
||||
searchResults.map((res, i) => {
|
||||
const Icon = res.icon;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => handleSelectResult(res.path)}
|
||||
className="flex items-center justify-between p-2.5 rounded-xl hover:bg-indigo-50/70 cursor-pointer transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0">
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-bold text-slate-900 group-hover:text-indigo-700 truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{res.title}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500 truncate">{res.desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[9px] font-bold px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 shrink-0">
|
||||
{res.category}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-4 text-center space-y-2">
|
||||
<span className="text-xs text-slate-500 block">
|
||||
{searchQuery.trim() ? `No matches found for "${searchQuery}"` : 'Type to search notes, faculty, routes & clubs'}
|
||||
</span>
|
||||
<div className="flex flex-wrap justify-center gap-1.5 pt-1">
|
||||
<button
|
||||
onClick={() => handleSelectResult('/notes')}
|
||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
||||
>
|
||||
Notes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSelectResult('/bus-routes')}
|
||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
||||
>
|
||||
Bus Routes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSelectResult('/faculty')}
|
||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
||||
>
|
||||
Faculty
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Header Status Bar */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Date Display Badge */}
|
||||
<div className="hidden md:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-100 text-slate-600 text-xs font-medium border border-slate-200/60">
|
||||
<Calendar className="w-3.5 h-3.5 text-purple-600" />
|
||||
<Calendar className="w-3.5 h-3.5 text-indigo-600" />
|
||||
<span>{currentDate}</span>
|
||||
</div>
|
||||
|
||||
{/* Notifications Icon */}
|
||||
<button
|
||||
className="relative p-2 rounded-xl bg-slate-100 hover:bg-purple-50 text-slate-600 hover:text-purple-600 transition-colors cursor-pointer border border-slate-200/60"
|
||||
title="Notifications"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-purple-600 ring-2 ring-white" />
|
||||
</button>
|
||||
{/* Functional Notifications Dropdown */}
|
||||
<div className="relative" ref={notifRef}>
|
||||
<button
|
||||
onClick={() => setIsNotifOpen((prev) => !prev)}
|
||||
className="relative p-2 rounded-xl bg-slate-100 hover:bg-indigo-50 text-slate-600 hover:text-indigo-600 transition-colors cursor-pointer border border-slate-200/60"
|
||||
title="Notifications"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-indigo-600 ring-2 ring-white" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Notifications Panel Dropdown */}
|
||||
{isNotifOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white rounded-2xl border border-slate-200 shadow-xl overflow-hidden z-50 p-3 text-left">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-slate-900" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Notifications
|
||||
</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700">
|
||||
{unreadCount} new
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={markAllAsRead}
|
||||
className="text-[11px] font-semibold text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
)}
|
||||
{notifications.length > 0 && (
|
||||
<button
|
||||
onClick={clearNotifications}
|
||||
className="text-[11px] font-semibold text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-72 overflow-y-auto custom-scrollbar pt-2 space-y-2">
|
||||
{notifications.length > 0 ? (
|
||||
notifications.map((n) => (
|
||||
<div
|
||||
key={n.id}
|
||||
className={`p-2.5 rounded-xl border transition-colors ${
|
||||
n.read ? 'bg-slate-50/60 border-slate-100' : 'bg-indigo-50/40 border-indigo-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-bold text-slate-900 leading-snug" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{n.title}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 font-medium">{n.time}</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-600 leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{n.message}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="p-6 text-center text-xs text-slate-400">
|
||||
No new notifications
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Auth Quick Badge */}
|
||||
{isAuthenticated && user ? (
|
||||
@@ -102,11 +383,11 @@ export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<img
|
||||
src={user.pictureUrl}
|
||||
alt={user.name}
|
||||
className="w-8 h-8 rounded-full border border-purple-300 object-cover"
|
||||
className="w-8 h-8 rounded-full border border-indigo-300 object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-purple-600 text-white font-bold text-xs flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-600 text-white font-bold text-xs flex items-center justify-center">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
@@ -114,7 +395,7 @@ export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<span className="text-xs font-bold text-slate-800 leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-purple-600 font-semibold">
|
||||
<span className="text-[10px] text-indigo-600 font-semibold">
|
||||
{isVerifiedStudent ? 'Verified Student' : 'RIT Account'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -122,7 +403,7 @@ export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
) : (
|
||||
<button
|
||||
onClick={loginWithGoogle}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-purple-600 hover:bg-purple-700 text-white text-xs font-bold shadow-sm transition-all cursor-pointer"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold shadow-sm transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<LogIn className="w-3.5 h-3.5" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
Code2,
|
||||
Trophy,
|
||||
Rocket,
|
||||
ShieldAlert,
|
||||
LogOut,
|
||||
LogIn,
|
||||
ShieldCheck,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
Menu,
|
||||
GitBranch,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
@@ -28,13 +27,14 @@ import { useAuth } from '@/context/AuthContext';
|
||||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
path: string;
|
||||
icon: any;
|
||||
badge?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
@@ -43,9 +43,13 @@ interface NavGroup {
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
export default function Sidebar({
|
||||
isOpen,
|
||||
onToggle,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
|
||||
|
||||
const checkIsActive = (linkPath: string) => {
|
||||
@@ -72,7 +76,7 @@ export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
{ label: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||
{ label: 'Notes & PYQs', path: '/notes', icon: BookOpen },
|
||||
{ label: 'Student Toolkit', path: '/notes?section=toolkit', icon: Wrench },
|
||||
{ label: 'RIT AI Chatbot', path: '/ai-assistant', icon: Bot, badge: 'AI' },
|
||||
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -88,9 +92,9 @@ export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
title: 'COMMUNITY & DEV',
|
||||
items: [
|
||||
{ label: 'Freshers Q&A', path: '/community', icon: MessageSquare },
|
||||
{ label: 'Dev Collab Hub', path: '/collab', icon: Code2 },
|
||||
{ label: 'Dev Collab Hub', path: '/collab', icon: Code2, highlight: true },
|
||||
{ label: 'LeetCode Board', path: '/leetcode', icon: Trophy },
|
||||
{ label: 'RAISE Incubator', path: '/raise', icon: Rocket, highlight: true },
|
||||
{ label: 'RAISE Incubator', path: '/raise', icon: Rocket },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -110,75 +114,88 @@ export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Main Sidebar Container */}
|
||||
{/* Main Sidebar Container - Sleek Professional Deep Indigo */}
|
||||
<aside
|
||||
className={`fixed top-0 left-0 z-50 h-screen w-64 xl:w-72 bg-gradient-to-b from-[#1E1B4B] via-[#2E1065] to-[#3B0764] text-white transition-transform duration-300 ease-in-out lg:translate-x-0 ${
|
||||
className={`fixed top-0 left-0 z-50 h-screen bg-[#0F172A] text-white transition-all duration-300 ease-in-out lg:translate-x-0 ${
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
} flex flex-col justify-between border-r border-white/10 shadow-[8px_0_32px_rgba(0,0,0,0.3)]`}
|
||||
} ${
|
||||
isCollapsed ? 'w-20' : 'w-64 xl:w-72'
|
||||
} flex flex-col justify-between border-r border-slate-800 shadow-[8px_0_30px_rgba(0,0,0,0.3)]`}
|
||||
>
|
||||
{/* Top Header & Branding */}
|
||||
<div className="p-6 flex flex-col gap-6">
|
||||
{/* Top Branding & Collapse Button */}
|
||||
<div className="p-4 sm:p-5 flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
<div className="relative p-2 rounded-2xl bg-gradient-to-br from-purple-500/30 to-indigo-500/20 border border-white/20 shadow-lg group-hover:scale-105 transition-transform">
|
||||
<Link to="/" className="flex items-center gap-3 group min-w-0">
|
||||
<div className="p-2 rounded-2xl bg-indigo-950/80 border border-indigo-500/20 shadow-sm shrink-0">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="RIT Logo"
|
||||
className="w-8 h-8 object-contain rounded-full"
|
||||
className="w-7 h-7 object-contain rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-extrabold text-white text-xl tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{!isCollapsed && (
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="font-bold text-white text-lg tracking-tight truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
RIT Portal
|
||||
</span>
|
||||
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-purple-500/30 text-purple-200 border border-purple-400/30">
|
||||
v2.0
|
||||
<span className="text-[10px] text-indigo-300/70 font-medium tracking-wider uppercase truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Freshers Hub
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-medium text-purple-200/70 tracking-wider uppercase" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Rajalakshmi Tech
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Mobile Close Button */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="lg:hidden p-2 rounded-xl bg-white/10 hover:bg-white/20 text-purple-200 transition-colors"
|
||||
className="lg:hidden p-1.5 rounded-xl bg-slate-800 text-slate-200 hover:bg-slate-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Desktop Collapse Toggle Button */}
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="hidden lg:flex p-1.5 rounded-xl bg-slate-800/80 hover:bg-indigo-900/60 text-indigo-300 hover:text-white border border-slate-700/60 transition-colors"
|
||||
title={isCollapsed ? 'Expand Sidebar' : 'Collapse Sidebar'}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Pitch CTA inside Sidebar */}
|
||||
<Link
|
||||
to="/raise"
|
||||
className="w-full flex items-center justify-between p-3.5 rounded-2xl bg-gradient-to-r from-purple-600 via-indigo-600 to-purple-700 hover:from-purple-500 hover:to-indigo-500 border border-purple-400/30 shadow-md shadow-purple-900/40 text-white group transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-xl bg-amber-400/20 border border-amber-300/40 flex items-center justify-center text-amber-300">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
{/* Quick Dev Collab Launch CTA inside Sidebar (Expanded only) */}
|
||||
{!isCollapsed && (
|
||||
<Link
|
||||
to="/collab"
|
||||
className="w-full flex items-center justify-between p-3 rounded-2xl bg-indigo-600 hover:bg-indigo-500 border border-indigo-400/30 text-white group transition-all duration-200 shadow-md shadow-indigo-950/50"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-7 h-7 rounded-xl bg-white/20 border border-white/30 flex items-center justify-center text-white shrink-0">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-col text-left min-w-0">
|
||||
<span className="text-xs font-bold leading-tight truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Dev Collab Hub
|
||||
</span>
|
||||
<span className="text-[10px] text-indigo-100/80 truncate">Co-Developer Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-xs font-bold leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
RAISE Incubator
|
||||
</span>
|
||||
<span className="text-[10px] text-purple-200/80">Pitch Startup Idea</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-purple-200 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4 text-indigo-200 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Navigation Groups */}
|
||||
<nav className="flex flex-col gap-6 overflow-y-auto max-h-[calc(100vh-340px)] custom-scrollbar pr-1">
|
||||
<nav className="flex flex-col gap-5 overflow-y-auto max-h-[calc(100vh-320px)] custom-scrollbar pr-0.5">
|
||||
{navGroups.map((group) => (
|
||||
<div key={group.title} className="flex flex-col gap-1">
|
||||
<span
|
||||
className="px-3 text-[10px] font-bold text-purple-300/60 tracking-widest uppercase mb-1"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{group.title}
|
||||
</span>
|
||||
{!isCollapsed && (
|
||||
<span
|
||||
className="px-3 text-[10px] font-bold text-slate-400/70 tracking-widest uppercase mb-1"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{group.title}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{group.items.map((item) => {
|
||||
const isActive = checkIsActive(item.path);
|
||||
@@ -188,32 +205,27 @@ export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`relative flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-semibold transition-all duration-200 group ${
|
||||
title={isCollapsed ? item.label : undefined}
|
||||
className={`relative flex items-center ${
|
||||
isCollapsed ? 'justify-center px-0 py-3' : 'justify-between px-3 py-2.5'
|
||||
} rounded-xl text-xs font-semibold transition-all duration-200 group ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-purple-600/90 via-purple-700/80 to-indigo-800/90 text-white shadow-lg shadow-purple-950/50 border border-purple-400/30'
|
||||
: 'text-purple-200/80 hover:text-white hover:bg-white/10'
|
||||
? 'bg-indigo-600 text-white shadow-sm border border-indigo-400/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-800/60'
|
||||
}`}
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon
|
||||
className={`w-4 h-4 transition-colors ${
|
||||
isActive
|
||||
? 'text-white'
|
||||
: 'text-purple-300/70 group-hover:text-purple-200'
|
||||
className={`w-4 h-4 shrink-0 ${
|
||||
isActive ? 'text-white' : 'text-slate-400 group-hover:text-slate-200'
|
||||
}`}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
{!isCollapsed && <span>{item.label}</span>}
|
||||
</div>
|
||||
|
||||
{item.badge && (
|
||||
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded-md bg-amber-500/20 text-amber-300 border border-amber-400/30">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{item.highlight && !isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-rose-400 animate-pulse" />
|
||||
{!isCollapsed && item.highlight && !isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-indigo-400 animate-pulse" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
@@ -224,54 +236,55 @@ export default function Sidebar({ isOpen, onToggle }: SidebarProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer User Profile & Auth Section */}
|
||||
<div className="p-4 border-t border-white/10 bg-black/20 backdrop-blur-md">
|
||||
<div className="p-3 border-t border-slate-800 bg-slate-950/60">
|
||||
{isAuthenticated && user ? (
|
||||
<div className="flex items-center justify-between p-2.5 rounded-2xl bg-white/5 border border-white/10">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={`flex items-center ${isCollapsed ? 'justify-center p-2' : 'justify-between p-2.5'} rounded-2xl bg-white/5 border border-white/10`}>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
{user.pictureUrl ? (
|
||||
<img
|
||||
src={user.pictureUrl}
|
||||
alt={user.name}
|
||||
className="w-9 h-9 rounded-xl border border-purple-400/40 object-cover shrink-0"
|
||||
className="w-8 h-8 rounded-xl border border-indigo-400/40 object-cover shrink-0"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-bold text-sm shrink-0">
|
||||
<div className="w-8 h-8 rounded-xl bg-indigo-600 text-white font-bold text-xs flex items-center justify-center shrink-0">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-bold text-white truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{user.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-[10px] text-purple-200/70 truncate">
|
||||
{isVerifiedStudent ? (
|
||||
<span className="text-emerald-400 font-semibold flex items-center gap-0.5">
|
||||
<ShieldCheck className="w-3 h-3" /> Verified Student
|
||||
</span>
|
||||
) : (
|
||||
<span>RIT Account</span>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div className="flex flex-col min-w-0 text-left">
|
||||
<span className="text-xs font-bold text-white truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 truncate">
|
||||
{isVerifiedStudent ? 'Verified Student' : 'RIT Account'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
title="Sign Out"
|
||||
className="p-2 rounded-xl hover:bg-white/10 text-purple-300 hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
{!isCollapsed && (
|
||||
<button
|
||||
onClick={logout}
|
||||
title="Sign Out"
|
||||
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={loginWithGoogle}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 rounded-2xl bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-bold text-xs shadow-md border border-purple-400/30 transition-all cursor-pointer"
|
||||
title={isCollapsed ? 'Student Sign In' : undefined}
|
||||
className={`w-full flex items-center justify-center gap-2 ${
|
||||
isCollapsed ? 'p-2.5' : 'p-2.5'
|
||||
} rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition-all cursor-pointer`}
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
<span>Student Sign In</span>
|
||||
<LogIn className="w-4 h-4 shrink-0" />
|
||||
{!isCollapsed && <span>Student Sign In</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user