feat: integrate all frontend modules and premium UI

This commit is contained in:
Amudieshwar A G
2026-07-23 14:22:47 +05:30
parent 27999a5bea
commit 5a10192ef0
34 changed files with 2731 additions and 889 deletions

View File

@@ -1,7 +1,11 @@
# ─── DATABASE CONNECTION CONFIGURATION ───
spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=disable
spring.datasource.username=postgres
<<<<<<< Updated upstream
spring.datasource.password=your_local_postgres_password_here
=======
spring.datasource.password=${DB_PASSWORD:Amudiesh22@.}
>>>>>>> Stashed changes
# ─── JPA / HIBERNATE SETTINGS ───
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/clubs/fusion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/clubs/helios.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/clubs/infinitus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
public/clubs/mediastic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/clubs/nippon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
public/clubs/nss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
public/clubs/podx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/clubs/rotaract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/clubs/steam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/clubs/techspark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
public/clubs/vaarithi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
public/clubs/wec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/clubs/wistem.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/clubs/yuva.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
public/images.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

BIN
public/video/RIT Video.mp4 Normal file

Binary file not shown.

View File

@@ -1,4 +1,5 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MainLayout from '@/layouts/MainLayout';
import Home from '@/pages/Home/Home';
@@ -18,10 +19,21 @@ const queryClient = new QueryClient({
},
});
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ScrollToTop />
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />

0
src/assets/campus.mp4 Normal file
View File

View File

@@ -1,5 +1,7 @@
import { motion } from 'framer-motion';
import { Mail, Phone, Building2, User } from 'lucide-react';
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Mail, Phone, Building2, User, X, BookOpen, ChevronRight, Monitor, Command, GraduationCap, Briefcase, Microscope, Clock } from 'lucide-react';
import type { Faculty } from '@/types';
interface FacultyCardProps {
@@ -16,6 +18,9 @@ const AVATAR_COLORS = [
];
export default function FacultyCard({ faculty }: FacultyCardProps) {
const [showModal, setShowModal] = useState(false);
const [showEmailOptions, setShowEmailOptions] = useState(false);
const colorIndex = parseInt(faculty.id) % AVATAR_COLORS.length;
const [from, to] = AVATAR_COLORS[colorIndex];
const initials = faculty.name
@@ -24,26 +29,38 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
.map((n) => n[0])
.join('');
const mailOptions = [
{ name: 'Default Mail App', url: `mailto:${faculty.email}`, icon: Monitor, color: 'text-gray-700', bg: 'bg-gray-100' },
{ name: 'Gmail', url: `https://mail.google.com/mail/?view=cm&fs=1&to=${faculty.email}`, icon: Mail, color: 'text-red-500', bg: 'bg-red-50' },
{ name: 'Outlook', url: `https://outlook.office.com/mail/deeplink/compose?to=${faculty.email}`, icon: Mail, color: 'text-blue-600', bg: 'bg-blue-50' },
{ name: 'Yahoo Mail', url: `https://compose.mail.yahoo.com/?to=${faculty.email}`, icon: Mail, color: 'text-purple-600', bg: 'bg-purple-50' },
];
return (
<>
<motion.div
whileHover={{ y: -5, boxShadow: '0 16px 40px -8px rgba(0,0,0,0.12)' }}
transition={{ duration: 0.25 }}
className="bg-white border border-[#E5E7EB] rounded-2xl p-5 flex flex-col gap-4"
whileHover={{ y: -6, boxShadow: '0 20px 40px -10px rgba(0,0,0,0.1)' }}
transition={{ duration: 0.3 }}
className="bg-white border border-[#E8ECF4] rounded-[20px] p-5 flex flex-col gap-4 relative"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
>
{/* Avatar + Name */}
<div className="flex items-center gap-4">
<div className="relative">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0"
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0 shadow-sm"
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
>
{initials}
</div>
{/* Online Indicator */}
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#22C55E] border-2 border-white rounded-full z-10 shadow-sm" />
</div>
<div>
<h3 className="font-semibold text-[#1E293B] text-sm leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
{faculty.name}
</h3>
<p className="text-xs text-[#F97316] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
<p className="text-xs text-[#FF7A00] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
{faculty.designation}
</p>
</div>
@@ -55,42 +72,316 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
</div>
{faculty.office && (
<div className="flex items-center gap-2 text-xs text-[#475569]">
<User className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office}</span>
</div>
)}
{faculty.specialization && (
<div className="text-xs text-[#94A3B8] pl-5" style={{ fontFamily: 'Inter, sans-serif' }}>
{faculty.specialization}
</div>
)}
</div>
{/* Contact */}
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB]">
<a
href={`mailto:${faculty.email}`}
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
{/* Contact & Actions */}
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB] mt-auto">
<button
onClick={() => setShowEmailOptions(true)}
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50 border border-transparent hover:border-gray-200"
style={{ fontFamily: 'Poppins, sans-serif' }}
aria-label={`Email ${faculty.name}`}
>
<Mail className="w-3.5 h-3.5" />
Email
</a>
</button>
<button
onClick={() => setShowModal(true)}
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
>
View Profile
<ChevronRight className="w-3.5 h-3.5" />
</button>
</div>
</motion.div>
{/* Profile Modal */}
{createPortal(
<AnimatePresence>
{showModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowModal(false)}
className="absolute inset-0 bg-[#0F172A]/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", duration: 0.4, bounce: 0.2 }}
className="relative w-full max-w-5xl bg-[#F8FAFC] shadow-2xl overflow-y-auto"
style={{ maxHeight: '90vh', borderRadius: '24px' }}
>
<button
onClick={() => setShowModal(false)}
className="absolute top-4 right-4 p-2 bg-white hover:bg-gray-100 rounded-full transition-colors z-50 shadow-sm border border-gray-200"
>
<X className="w-5 h-5 text-gray-500" />
</button>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full p-4 sm:p-8 relative">
{/* LEFT PANEL */}
<div className="lg:col-span-4 h-full relative">
<div
className="rounded-[20px] shadow-sm border p-8 flex flex-col items-center relative lg:sticky lg:top-0"
style={{
background: `linear-gradient(135deg, ${from}10, ${to}10)`,
borderColor: `${from}20`
}}
>
<div
className="w-32 h-32 rounded-full flex items-center justify-center text-white font-bold text-5xl mb-5 shadow-lg"
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
>
{initials}
</div>
<h2 className="text-2xl font-bold text-center text-[#1E293B] mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
{faculty.name}
</h2>
<p className="text-base font-semibold text-[#FF7A00] text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
{faculty.designation}
</p>
<p className="text-xs font-semibold text-[#64748B] text-center mt-2 uppercase tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
{faculty.department}
</p>
<hr className="w-full my-6" style={{ borderColor: `${from}20` }} />
<div className="w-full flex flex-col gap-3">
<button
onClick={() => setShowEmailOptions(true)}
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 transition-colors border group text-left w-full"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.6)', borderColor: `${from}15` }}
>
<div className="w-10 h-10 rounded-full bg-[#FFF7ED] flex items-center justify-center group-hover:bg-[#F97316] transition-colors shrink-0">
<Mail className="w-4 h-4 text-[#F97316] group-hover:text-white transition-colors" />
</div>
<div className="overflow-hidden">
<p className="text-[11px] font-semibold text-[#94A3B8] uppercase tracking-wider mb-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Email Address</p>
<p className="text-sm font-semibold text-[#1E293B] truncate" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.email}</p>
</div>
</button>
{faculty.phone && (
<a
href={`tel:${faculty.phone}`}
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50"
style={{ fontFamily: 'Poppins, sans-serif' }}
aria-label={`Call ${faculty.name}`}
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 transition-colors border group w-full"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.6)', borderColor: `${from}15` }}
>
<Phone className="w-3.5 h-3.5" />
Call
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center group-hover:bg-blue-500 transition-colors shrink-0">
<Phone className="w-4 h-4 text-blue-500 group-hover:text-white transition-colors" />
</div>
<div className="overflow-hidden">
<p className="text-[11px] font-semibold text-[#94A3B8] uppercase tracking-wider mb-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Phone Number</p>
<p className="text-sm font-semibold text-[#1E293B] truncate" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.phone}</p>
</div>
</a>
)}
</div>
</div>
</div>
{/* RIGHT PANEL */}
<div className="lg:col-span-8 flex flex-col gap-6">
{/* Section 1: Professional Details */}
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center shrink-0">
<User className="w-5 h-5 text-[#FF7A00]" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Professional Details</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Department</p>
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Specialization</p>
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.specialization || 'Not specified'}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Qualification</p>
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.qualification || 'Not specified'}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Experience</p>
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.experience || 'Not specified'}</p>
</div>
{faculty.joinedInstitution && (
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50 md:col-span-2">
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Joined Institution</p>
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.joinedInstitution}</p>
</div>
)}
</div>
</div>
{/* Section 2: Academic Info */}
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
<BookOpen className="w-5 h-5 text-indigo-500" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Academic Information</h3>
</div>
<div className="flex flex-col gap-4">
<div className="p-5 rounded-xl bg-indigo-50/30 border border-indigo-100/50">
<p className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Subjects Handling</p>
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.subjectsHandling || 'Not specified'}</p>
</div>
<div className="p-5 rounded-xl bg-teal-50/30 border border-teal-100/50">
<p className="text-xs font-bold text-teal-500 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Research Areas</p>
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas || 'Not specified'}</p>
</div>
{faculty.studentsGuided && (
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50">
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Students Guided</p>
<div className="flex gap-8">
<div className="flex flex-col">
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.ug}</span>
<span className="text-xs font-semibold text-[#64748B]">UG</span>
</div>
<div className="flex flex-col">
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.pg}</span>
<span className="text-xs font-semibold text-[#64748B]">PG</span>
</div>
<div className="flex flex-col">
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.phd}</span>
<span className="text-xs font-semibold text-[#64748B]">Ph.D.</span>
</div>
</div>
</div>
)}
</div>
</div>
{/* Section 3: Achievements (Render only if present) */}
{(faculty.achievements || faculty.publications) && (
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-pink-50 flex items-center justify-center shrink-0">
<GraduationCap className="w-5 h-5 text-pink-500" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Achievements</h3>
</div>
<div className="flex flex-col gap-6">
{faculty.achievements && faculty.achievements.length > 0 && (
<div>
<p className="text-xs font-bold text-pink-400 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Awards & Certifications</p>
<ul className="list-disc list-inside space-y-2">
{faculty.achievements.map((ach, i) => (
<li key={i} className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{ach}</li>
))}
</ul>
</div>
)}
{faculty.publications && faculty.publications.length > 0 && (
<div>
<p className="text-xs font-bold text-pink-400 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Recent Publications</p>
<ul className="list-disc list-inside space-y-2">
{faculty.publications.map((pub, i) => (
<li key={i} className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{pub}</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{/* Section 4: Office Info */}
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-emerald-500" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Office Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-5 rounded-xl bg-emerald-50/50 border border-emerald-100 flex items-start gap-4">
<Clock className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
<div>
<p className="text-xs font-bold text-emerald-600 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Hours</p>
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours || 'Not specified'}</p>
</div>
</div>
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50 flex items-start gap-4">
<Building2 className="w-6 h-6 text-blue-500 shrink-0 mt-0.5" />
<div>
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Location</p>
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office || 'Not specified'}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
)}
{/* Email Options Modal */}
{createPortal(
<AnimatePresence>
{showEmailOptions && (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowEmailOptions(false)}
className="absolute inset-0 bg-[#0F172A]/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", duration: 0.4, bounce: 0.2 }}
className="relative w-full max-w-sm bg-white rounded-3xl shadow-2xl p-6 flex flex-col gap-5"
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Choose Mail App</h3>
<p className="text-xs text-[#64748B] mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Compose email to {faculty.name}</p>
</div>
<button onClick={() => setShowEmailOptions(false)} className="p-2 rounded-full hover:bg-gray-100 transition-colors">
<X className="w-5 h-5 text-[#94A3B8]" />
</button>
</div>
<div className="flex flex-col gap-2">
{mailOptions.map((option, idx) => (
<a
key={idx}
href={option.url}
target={option.name !== 'Default Mail App' ? '_blank' : undefined}
rel={option.name !== 'Default Mail App' ? 'noopener noreferrer' : undefined}
onClick={() => setShowEmailOptions(false)}
className="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 border border-gray-100 transition-all group hover:border-gray-200"
>
<div className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${option.bg}`}>
<option.icon className={`w-4 h-4 ${option.color}`} />
</div>
<span className="text-sm font-semibold text-[#1E293B] group-hover:text-black" style={{ fontFamily: 'Inter, sans-serif' }}>
{option.name}
</span>
</a>
))}
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
)}
</>
);
}

View File

@@ -9,7 +9,7 @@ interface FeatureCardProps {
}
export default function FeatureCard({ feature }: FeatureCardProps) {
const IconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon];
const IconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>>)[feature.icon];
return (
<motion.div

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X, GraduationCap, Bell } from 'lucide-react';
import { Menu, X } from 'lucide-react';
import { NAV_LINKS } from '@/constants';
export default function Navbar() {
@@ -25,54 +25,38 @@ export default function Navbar() {
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className={`fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-6xl transition-all duration-300 ${
isScrolled
? 'shadow-[0_8px_32px_rgba(0,0,0,0.12)] bg-white/90'
: 'bg-white/80'
}`}
className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-6xl transition-all duration-300 shadow-[0_8px_32px_rgba(0,0,0,0.25)]"
style={{
background: 'linear-gradient(to right, rgba(15, 23, 42, 0.45) 0%, rgba(30, 41, 59, 0.35) 50%, rgba(15, 23, 42, 0.45) 100%)',
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
borderRadius: '20px',
border: '1px solid rgba(229,231,235,0.8)',
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
>
<div className="flex items-center justify-between px-5 py-2.5">
{/* Logo - RIT Event Hub Style */}
<Link to="/" className="flex items-center gap-2 group">
{/* Blue Circular Tree Icon */}
<motion.div
<a href="/" className="flex items-center gap-2 group">
{/* Logo Image */}
<motion.img
whileHover={{ scale: 1.05 }}
className="w-10 h-10 rounded-full flex items-center justify-center shrink-0"
style={{ background: 'linear-gradient(135deg, #1E40AF, #3B82F6)' }}
>
<svg viewBox="0 0 100 100" className="w-6.5 h-6.5 text-white" fill="none" stroke="currentColor" strokeWidth="6" strokeLinecap="round">
{/* Minimalist Tree Branch Representation */}
<path d="M50 85 V40" />
<path d="M50 65 Q65 55 75 60" />
<path d="M50 55 Q35 45 25 50" />
<path d="M50 45 Q70 30 65 15" />
<path d="M50 45 Q30 30 35 15" />
<circle cx="50" cy="38" r="8" fill="white" stroke="none" />
<circle cx="65" cy="15" r="5" fill="white" stroke="none" />
<circle cx="35" cy="15" r="5" fill="white" stroke="none" />
<circle cx="75" cy="60" r="5" fill="white" stroke="none" />
<circle cx="25" cy="50" r="5" fill="white" stroke="none" />
</svg>
</motion.div>
src="/logo.png"
alt="RIT Logo"
className="w-10 h-10 object-contain rounded-full shrink-0"
/>
<div className="flex items-center">
<span className="font-extrabold text-[#1E293B] text-2xl tracking-tighter" style={{ fontFamily: 'Poppins, sans-serif' }}>
<span className="font-extrabold text-white text-2xl tracking-tighter" style={{ fontFamily: 'Poppins, sans-serif' }}>
rit
</span>
<div className="h-7 w-[1px] bg-gray-300 mx-2" />
<div className="flex flex-col text-[8.5px] font-bold text-[#475569] leading-tight tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
<div className="h-7 w-[1px] bg-white/20 mx-2" />
<div className="flex flex-col text-[8.5px] font-bold text-slate-200 leading-tight tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
<span>RAJALAKSHMI</span>
<span>INSTITUTE OF</span>
<span>TECHNOLOGY</span>
</div>
</div>
</Link>
</a>
{/* Desktop Nav */}
<div className="hidden md:flex items-center gap-1">
@@ -85,14 +69,14 @@ export default function Navbar() {
className="relative px-4 py-2 rounded-xl text-xs font-semibold uppercase tracking-wider transition-all duration-200"
style={{
fontFamily: 'Poppins, sans-serif',
color: isActive ? '#F97316' : '#475569',
color: isActive ? '#FFFFFF' : '#CBD5E1',
}}
>
{isActive && (
<motion.div
layoutId="nav-active"
className="absolute inset-0 rounded-xl"
style={{ backgroundColor: '#FFF7ED' }}
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
transition={{ type: 'spring', bounce: 0.2, duration: 0.4 }}
/>
)}
@@ -104,35 +88,11 @@ export default function Navbar() {
{/* Right Side */}
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="hidden md:flex w-9 h-9 rounded-xl items-center justify-center text-[#94A3B8] hover:text-[#F97316] hover:bg-[#FFF7ED] transition-all"
aria-label="Notifications"
>
<Bell className="w-4.5 h-4.5" />
</motion.button>
{/* Profile styled as an orange LOG OUT pill button from Reference Image */}
<Link to="/profile" className="hidden md:block">
<motion.button
whileHover={{ scale: 1.03, boxShadow: '0 4px 15px rgba(249,115,22,0.3)' }}
whileTap={{ scale: 0.97 }}
className="px-5 py-2 rounded-full text-white font-bold text-[10.5px] tracking-wider uppercase transition-all"
style={{
fontFamily: 'Poppins, sans-serif',
background: 'linear-gradient(135deg, #F97316, #FB923C)',
}}
>
Profile
</motion.button>
</Link>
{/* Mobile Toggle */}
<motion.button
whileTap={{ scale: 0.9 }}
onClick={() => setIsMobileOpen(!isMobileOpen)}
className="md:hidden w-9 h-9 rounded-xl flex items-center justify-center text-[#475569] hover:bg-gray-100 transition-all"
className="md:hidden w-9 h-9 rounded-xl flex items-center justify-center text-slate-300 hover:text-white hover:bg-white/10 transition-all"
aria-label="Toggle menu"
>
{isMobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
@@ -150,7 +110,7 @@ export default function Navbar() {
transition={{ duration: 0.25 }}
className="overflow-hidden md:hidden"
>
<div className="px-4 pb-4 pt-2 flex flex-col gap-1 border-t border-gray-100">
<div className="px-4 pb-4 pt-2 flex flex-col gap-1 border-t border-white/10">
{NAV_LINKS.map((link) => {
const isActive = location.pathname === link.path;
return (
@@ -160,8 +120,8 @@ export default function Navbar() {
className="px-4 py-3 rounded-xl text-sm font-medium transition-all"
style={{
fontFamily: 'Inter, sans-serif',
color: isActive ? '#F97316' : '#475569',
backgroundColor: isActive ? '#FFF7ED' : 'transparent',
color: isActive ? '#FFFFFF' : '#CBD5E1',
backgroundColor: isActive ? '#F97316' : 'transparent',
}}
>
{link.label}

View File

@@ -6,17 +6,17 @@ export const NAV_LINKS = [
{ label: 'Notes', path: '/notes' },
{ label: 'AI Assistant', path: '/ai-assistant' },
{ label: 'Campus', path: '/campus' },
{ label: 'Events', path: '/events' },
{ label: 'Clubs', path: '/events' },
{ label: 'Community', path: '/community' },
];
// ─── Stats ────────────────────────────────────────────────────────────────────
export const STATS: Stat[] = [
{ value: 150, suffix: '+', label: 'PYQs', icon: 'FileText' },
{ value: 40, suffix: '+', label: 'Faculty', icon: 'Users' },
{ value: 25, suffix: '+', label: 'Clubs', icon: 'Star' },
{ value: 20, suffix: '+', label: 'Sports', icon: 'Trophy' },
{ value: 100, suffix: '+', label: 'Faculty', icon: 'Users' },
{ value: 18, suffix: '', label: 'Clubs', icon: 'Star' },
{ value: 24, suffix: '/7', label: 'AI Assistant', icon: 'Bot' },
{ value: 1000, suffix: '+', label: 'Students', icon: 'GraduationCap' },
{ value: 5000, suffix: '+', label: 'Students', icon: 'GraduationCap' },
];
// ─── Features ────────────────────────────────────────────────────────────────
@@ -77,9 +77,9 @@ export const FEATURES: Feature[] = [
},
{
id: 'events',
icon: 'Calendar',
title: 'Clubs & Events',
description: 'Discover clubs, upcoming events, workshops, and cultural programs at RIT.',
icon: 'Users',
title: 'Student Clubs',
description: 'Discover official RIT student clubs, technology domains, leadership, and contacts.',
path: '/events',
color: '#EF4444',
bgColor: '#FEF2F2',
@@ -179,55 +179,302 @@ export const FACULTY_DATA: Faculty[] = [
name: 'Dr. S. Ramesh Kumar',
designation: 'Professor & HOD',
department: 'Computer Science & Engineering',
email: 'ramesh@rit.edu',
office: 'CSE Block, Room 301',
specialization: 'Machine Learning, AI',
specialization: 'Machine Learning, Artificial Intelligence',
email: 'ramesh@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '18 Years',
subjectsHandling: 'Machine Learning, Artificial Intelligence, Data Mining',
researchAreas: 'AI, Deep Learning, Computer Vision',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43001',
},
{
id: '2',
name: 'Dr. P. Mahalakshmi',
designation: 'Associate Professor',
department: 'Electronics & Communication',
email: 'mahalakshmi@rit.edu',
office: 'ECE Block, Room 205',
specialization: 'VLSI Design, Embedded Systems',
},
{
id: '3',
name: 'Prof. R. Vijayakumar',
designation: 'Assistant Professor',
department: 'Mechanical Engineering',
email: 'vijay@rit.edu',
office: 'MECH Block, Room 102',
specialization: 'CAD/CAM, Robotics',
},
{
id: '4',
name: 'Dr. K. Anitha',
designation: 'Professor',
department: 'Information Technology',
email: 'anitha@rit.edu',
office: 'IT Block, Room 401',
specialization: 'Data Science, Cloud Computing',
},
{
id: '5',
name: 'Prof. M. Selvakumar',
designation: 'Associate Professor',
department: 'Civil Engineering',
email: 'selva@rit.edu',
office: 'CIVIL Block, Room 110',
specialization: 'Structural Engineering',
},
{
id: '6',
name: 'Dr. S. Priya',
designation: 'Assistant Professor',
department: 'Computer Science & Engineering',
email: 'priya@rit.edu',
office: 'CSE Block, Room 205',
specialization: 'Cyber Security, Networks',
specialization: 'Cyber Security, Computer Networks',
email: 'priya@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '8 Years',
subjectsHandling: 'Network Security, Computer Networks, Ethical Hacking',
researchAreas: 'Cyber Security, IoT Security',
officeHours: 'MonFri, 2:00 PM4:00 PM',
phone: '+91 98765 43002',
},
{
id: '3',
name: 'Dr. K. Aravind',
designation: 'Associate Professor',
department: 'Computer Science & Engineering',
office: 'CSE Block, Room 212',
specialization: 'Cloud Computing, Distributed Systems',
email: 'aravind@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '11 Years',
subjectsHandling: 'Cloud Computing, Distributed Systems, Operating Systems',
researchAreas: 'Cloud Infrastructure, Edge Computing',
officeHours: 'MonFri, 9:30 AM11:30 AM',
phone: '+91 98765 43003',
},
{
id: '6',
name: 'Dr. P. Mahalakshmi',
designation: 'Associate Professor',
department: 'Electronics & Communication Engineering',
office: 'ECE Block, Room 205',
specialization: 'VLSI Design, Embedded Systems',
email: 'mahalakshmi@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '13 Years',
subjectsHandling: 'VLSI Design, Embedded Systems',
researchAreas: 'Chip Design, IoT Devices',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43006',
},
{
id: '7',
name: 'Dr. V. Prakash',
designation: 'Professor',
department: 'Electronics & Communication Engineering',
office: 'ECE Block, Room 310',
specialization: 'Digital Signal Processing, IoT',
email: 'prakash@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '17 Years',
subjectsHandling: 'DSP, IoT, Microprocessors',
researchAreas: 'Wireless Systems, Signal Processing',
officeHours: 'MonFri, 2:00 PM4:00 PM',
phone: '+91 98765 43007',
},
{
id: '8',
name: 'Dr. A. Deepika',
designation: 'Assistant Professor',
department: 'Electronics & Communication Engineering',
office: 'ECE Block, Room 214',
specialization: 'Wireless Communication, 5G Networks',
email: 'deepika@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '6 Years',
subjectsHandling: 'Wireless Networks, Mobile Communication',
researchAreas: '5G, IoT Networks',
officeHours: 'MonFri, 9:00 AM11:00 AM',
phone: '+91 98765 43008',
},
{
id: '9',
name: 'Prof. R. Vijayakumar',
designation: 'Assistant Professor',
department: 'Mechanical Engineering',
office: 'MECH Block, Room 102',
specialization: 'CAD/CAM, Robotics',
email: 'vijayakumar@rit.edu',
qualification: 'B.E., M.E.',
experience: '9 Years',
subjectsHandling: 'CAD/CAM, Robotics',
researchAreas: 'Industrial Automation',
officeHours: 'MonFri, 11:00 AM1:00 PM',
phone: '+91 98765 43009',
},
{
id: '10',
name: 'Dr. S. Manikandan',
designation: 'Associate Professor',
department: 'Mechanical Engineering',
office: 'MECH Block, Room 118',
specialization: 'Thermal Engineering, Manufacturing',
email: 'manikandan@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '12 Years',
subjectsHandling: 'Manufacturing Technology, Thermal Engineering',
researchAreas: 'Renewable Energy, Thermal Systems',
officeHours: 'MonFri, 1:00 PM3:00 PM',
phone: '+91 98765 43010',
},
{
id: '11',
name: 'Prof. M. Selvakumar',
designation: 'Associate Professor',
department: 'Civil Engineering',
office: 'CIVIL Block, Room 110',
specialization: 'Structural Engineering',
email: 'selvakumar@rit.edu',
qualification: 'B.E., M.E.',
experience: '14 Years',
subjectsHandling: 'Structural Analysis, RCC Design',
researchAreas: 'Earthquake Resistant Structures',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43011',
},
{
id: '12',
name: 'Dr. J. Revathi',
designation: 'Assistant Professor',
department: 'Civil Engineering',
office: 'CIVIL Block, Room 123',
specialization: 'Environmental Engineering, Surveying',
email: 'revathi@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '8 Years',
subjectsHandling: 'Surveying, Environmental Engineering',
researchAreas: 'Water Resource Management',
officeHours: 'MonFri, 2:00 PM4:00 PM',
phone: '+91 98765 43012',
},
{
id: '13',
name: 'Dr. N. Hariharan',
designation: 'Professor & HOD',
department: 'Artificial Intelligence & Data Science',
office: 'AIDS Block, Room 501',
specialization: 'Deep Learning, Data Mining',
email: 'hariharan@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '19 Years',
subjectsHandling: 'Deep Learning, Data Mining, AI',
researchAreas: 'Artificial Intelligence, Predictive Analytics',
officeHours: 'MonFri, 9:30 AM11:30 AM',
phone: '+91 98765 43013',
},
{
id: '14',
name: 'Dr. P. Divya',
designation: 'Assistant Professor',
department: 'Artificial Intelligence & Data Science',
office: 'AIDS Block, Room 505',
specialization: 'Natural Language Processing, Computer Vision',
email: 'divya@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '7 Years',
subjectsHandling: 'NLP, Computer Vision',
researchAreas: 'Generative AI, Image Processing',
officeHours: 'MonFri, 1:00 PM3:00 PM',
phone: '+91 98765 43014',
},
{
id: '15',
name: 'Dr. L. Karthikeyan',
designation: 'Associate Professor',
department: 'Electrical & Electronics Engineering',
office: 'EEE Block, Room 303',
specialization: 'Power Systems, Renewable Energy',
email: 'karthikeyan@rit.edu',
qualification: 'B.E., M.E., Ph.D.',
experience: '15 Years',
subjectsHandling: 'Power Systems, Electrical Machines',
researchAreas: 'Smart Grid, Solar Energy',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43015',
},
{
id: '16',
name: 'Dr. M. Murugan',
designation: 'Associate Professor',
department: 'Computer Science & Engineering',
office: 'CSE Block, Room 218',
specialization: 'Artificial Intelligence, Machine Learning',
email: 'murugan@rit.edu',
qualification: 'B.E. (CSE), M.E. (Computer Science & Engineering), Ph.D. (Artificial Intelligence)',
experience: '13 Years',
subjectsHandling: 'Artificial Intelligence, Machine Learning, Python Programming, Data Mining',
researchAreas: 'Deep Learning, Computer Vision, Natural Language Processing, Explainable AI',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43125',
},
{
id: '17',
name: 'Dr. R. Balamurugan',
designation: 'Associate Professor',
department: 'Computer Science & Business Systems',
office: 'CSBS Block, Room 305',
specialization: 'Business Analytics, Data Science',
email: 'balamurugan@rit.edu',
qualification: 'B.E. (CSE), M.B.A., M.E. (Software Engineering), Ph.D. (Business Analytics)',
experience: '12 Years',
subjectsHandling: 'Business Analytics, Data Analytics, Database Management Systems, Python for Business Applications',
researchAreas: 'Business Intelligence, Data Analytics, Digital Transformation, Decision Support Systems',
officeHours: 'MonFri, 9:30 AM11:30 AM',
phone: '+91 98765 43221',
},
{
id: '18',
name: 'Dr. K. Nandhini',
designation: 'Assistant Professor',
department: 'Artificial Intelligence & Machine Learning',
office: 'AIML Block, Room 412',
specialization: 'Artificial Intelligence, Machine Learning',
email: 'nandhini@rit.edu',
qualification: 'B.E. (CSE), M.Tech. (Artificial Intelligence), Ph.D. (Machine Learning)',
experience: '8 Years',
subjectsHandling: 'Artificial Intelligence, Machine Learning, Deep Learning, Python Programming',
researchAreas: 'Generative AI, Computer Vision, Explainable AI, Neural Networks',
officeHours: 'MonFri, 2:00 PM4:00 PM',
phone: '+91 98765 43222',
},
{
id: '19',
name: 'Dr. S. Rajeshwari',
designation: 'Professor',
department: 'Computer Science & Business Systems',
office: 'CSBS Block, Room 302',
specialization: 'Business Intelligence, Enterprise Systems',
email: 'rajeshwari@rit.edu',
qualification: 'B.E. (CSE), M.E. (Computer Science), Ph.D. (Business Intelligence)',
experience: '16 Years',
subjectsHandling: 'Business Intelligence, Enterprise Systems, Software Engineering, Project Management',
researchAreas: 'Business Intelligence, Enterprise Computing, ERP Systems',
officeHours: 'MonFri, 10:00 AM12:00 PM',
phone: '+91 98765 43223',
},
{
id: '20',
name: 'Dr. V. Pradeep Kumar',
designation: 'Assistant Professor',
department: 'Computer Science & Business Systems',
office: 'CSBS Block, Room 308',
specialization: 'FinTech, Cloud Computing',
email: 'pradeepkumar@rit.edu',
qualification: 'B.E. (IT), M.Tech. (Cloud Computing), Ph.D. (Financial Technology)',
experience: '7 Years',
subjectsHandling: 'Cloud Computing, FinTech Applications, Data Structures, Java Programming',
researchAreas: 'Cloud Security, Financial Analytics, Blockchain Technology',
officeHours: 'MonFri, 1:00 PM3:00 PM',
phone: '+91 98765 43224',
},
{
id: '21',
name: 'Dr. P. Harini',
designation: 'Associate Professor',
department: 'Artificial Intelligence & Machine Learning',
office: 'AIML Block, Room 408',
specialization: 'Deep Learning, Computer Vision',
email: 'harini@rit.edu',
qualification: 'B.E. (CSE), M.Tech. (Artificial Intelligence), Ph.D. (Computer Vision)',
experience: '10 Years',
subjectsHandling: 'Deep Learning, Computer Vision, Image Processing, Python Programming',
researchAreas: 'Medical Image Analysis, Deep Neural Networks, AI in Healthcare',
officeHours: 'MonFri, 9:00 AM11:00 AM',
phone: '+91 98765 43225',
},
{
id: '22',
name: 'Dr. A. Vignesh',
designation: 'Assistant Professor',
department: 'Artificial Intelligence & Machine Learning',
office: 'AIML Block, Room 415',
specialization: 'Natural Language Processing, Generative AI',
email: 'vignesh@rit.edu',
qualification: 'B.E. (CSE), M.E. (Artificial Intelligence), Ph.D. (Natural Language Processing)',
experience: '6 Years',
subjectsHandling: 'Natural Language Processing, Generative AI, Machine Learning, Data Science',
researchAreas: 'Large Language Models, Conversational AI, Explainable AI',
officeHours: 'MonFri, 2:00 PM4:00 PM',
phone: '+91 98765 43226',
}
];
// ─── Events ──────────────────────────────────────────────────────────────────
@@ -280,12 +527,300 @@ export const EVENTS_DATA: Event[] = [
// ─── Clubs ───────────────────────────────────────────────────────────────────
export const CLUBS_DATA: Club[] = [
{ id: '1', name: 'Coding Club', description: 'For programming enthusiasts', category: 'Technical', members: 120 },
{ id: '2', name: 'Robotics Club', description: 'Build and code robots', category: 'Technical', members: 85 },
{ id: '3', name: 'Photography Club', description: 'Capture campus moments', category: 'Creative', members: 60 },
{ id: '4', name: 'Music Club', description: 'Classical and contemporary music', category: 'Cultural', members: 95 },
{ id: '5', name: 'Drama Club', description: 'Theatre and performing arts', category: 'Cultural', members: 45 },
{ id: '6', name: 'NSS', description: 'National Service Scheme', category: 'Social', members: 200 },
{
id: 'stellar_space_tech',
name: 'STELLAR SPACE TECH CLUB',
description: 'Space technology, astronomy, satellite systems, and space science exploration at RIT.',
details: 'RIT Stellar Space Tech Club brings together space enthusiasts, students, and aspiring aerospace engineers to explore astronomy, satellite communication, rocketry, and cutting-edge space technology through workshops, observational sessions, and technical projects.',
category: 'Technical',
members: 94,
presidentName: 'Student Space Lead',
year: 'Senior Year',
contactEmail: 'spacetech@ritchennai.edu.in',
contactPhone: '+91 98765 43210',
icon: 'Rocket',
logoUrl: '/clubs/stellar_space_tech.png',
socialLinks: {
instagram: 'https://www.instagram.com/stellarspacetechclub/',
},
},
{
id: 'podx',
name: 'podX-RIT (PODCAST CLUB)',
description: 'Real talks. Raw feels. Because every story deserves to be heard. Official Podcast Club of RIT.',
details: 'podX-RIT is the official Podcast and Radio Club of Rajalakshmi Institute of Technology. Bringing student stories, Switcheroo interviews, RJ sessions, and inspiring student & faculty conversations to life.',
category: 'Creative',
members: 1097,
presidentName: 'Lead RJ / Podcaster',
year: 'Senior Year',
contactEmail: 'podx@ritchennai.edu.in',
contactPhone: '+91 98765 43211',
icon: 'Mic',
logoUrl: '/clubs/podx.png',
socialLinks: {
instagram: 'https://www.instagram.com/podxrit/',
website: 'https://linktr.ee/podxrit',
},
},
{
id: 'nss',
name: 'NSS OF RIT',
description: 'National Service Scheme "Not Me, But You." Official Social Service Club of RIT.',
details: 'NSS of Rajalakshmi Institute of Technology engages students in community service, social awareness drives, blood donation camps, road safety campaigns, and nation-building initiatives.',
category: 'Social',
members: 1306,
presidentName: 'NSS Student Lead',
year: 'Final Year',
contactEmail: 'nss@ritchennai.edu.in',
contactPhone: '+91 98765 43212',
icon: 'HeartHandshake',
logoUrl: '/clubs/nss.png',
socialLinks: {
instagram: 'https://www.instagram.com/nss.rit/',
},
},
{
id: 'artist_league',
name: 'ARTIST LEAGUE (CULTURAL CELL)',
description: 'Official Cultural Cell of RIT covering Dance (R-Square), Band (Euphoria), Rap & Classical.',
details: 'Artist League is the umbrella cultural organization of RIT incorporating R-Square (Dance), Euphoria (Band), Raptology (Rap), and Classic Beat (Classical). It organizes cultural fests, talent shows, and 60 Seconds to Fame competitions.',
category: 'Creative',
members: 1093,
presidentName: 'Cultural Secretary',
year: 'Final Year',
contactEmail: 'artistleague@ritchennai.edu.in',
contactPhone: '+91 98765 43213',
icon: 'Sparkles',
logoUrl: '/clubs/artist_league.png',
socialLinks: {
instagram: 'https://www.instagram.com/artist_league_rit/',
},
},
{
id: 'infinitus',
name: 'INFINITUS CLUB (MATHS CLUB)',
description: 'Where Logic Meets Innovation. Mathematics workshops, PiDoku, GraphQuest & tech competitions.',
details: 'Infinitus Club is the official Mathematics & Aptitude Club of RIT, hosting Pi-Quest, PiDoku Decode π, GraphQuest, logic puzzles, speed calculations, and analytical problem-solving workshops.',
category: 'Technical',
members: 557,
presidentName: 'Mathematics Lead',
year: 'Third Year',
contactEmail: 'infinitus@ritchennai.edu.in',
contactPhone: '+91 98765 43214',
icon: 'Calculator',
logoUrl: '/clubs/infinitus.png',
socialLinks: {
instagram: 'https://www.instagram.com/infinitus_club.rit/',
whatsapp: 'https://chat.whatsapp.com/LeCKYtgzdzJ5E6sL0WjYIHf',
},
},
{
id: 'vaarithi',
name: 'VAARITHI TAMIL CLUB',
description: 'இராசலட்சுமி தொழில்நுட்பக் கல்லூரியின் மாணவர் தமிழ் மன்றம் (RIT Tamil Mandram).',
details: 'Vaarithi Muthamizh Mandram of RIT celebrates Tamil literature, poetry, debate (Tamizhsudar), Tamil New Year celebrations, and preserving rich Tamil culture and heritage.',
category: 'Creative',
members: 848,
presidentName: 'Tamil Mandram Lead',
year: 'Final Year',
contactEmail: 'vaarithi@ritchennai.edu.in',
contactPhone: '+91 98765 43215',
icon: 'BookOpen',
logoUrl: '/clubs/vaarithi.png',
socialLinks: {
instagram: 'https://www.instagram.com/vaarithi_rit/',
},
},
{
id: 'fusion',
name: 'FUSION CLUB (ENGLISH & LITERATURE)',
description: 'A platform to delve into the beauty of language, literature, essays, and power of words.',
details: 'Fusion Club of RIT is the English Literary Club organizing essay writing contests, Lips Dont Lie games, Fusion Treasure Hunt, debates, and creative writing workshops.',
category: 'Creative',
members: 815,
presidentName: 'Literary President',
year: 'Third Year',
contactEmail: 'fusionclub@ritchennai.edu.in',
contactPhone: '+91 98765 43216',
icon: 'Languages',
logoUrl: '/clubs/fusion.png',
socialLinks: {
instagram: 'https://www.instagram.com/fusionclub_rit/',
},
},
{
id: 'nippon',
name: 'NIPPON CLUB (JAPANESE CULTURE)',
description: 'Celebrating Japanese culture, language, anime, movie review nights & vibrant community.',
details: 'Nippon Club of RIT brings Japanese language learning, Kizuna Canvas relay drawing, Koe Nashi voice challenge, anime screenings, and Japanese cultural immersion to RIT students.',
category: 'Creative',
members: 649,
presidentName: 'Nippon President',
year: 'Third Year',
contactEmail: 'nippon@ritchennai.edu.in',
contactPhone: '+91 98765 43217',
icon: 'Globe',
logoUrl: '/clubs/nippon.png',
socialLinks: {
instagram: 'https://www.instagram.com/nippon_rit/',
},
},
{
id: 'unnat_bharat',
name: 'RIT UNNAT BHARAT ABHIYAN (UBA)',
description: 'Unite, socialize, thrive together! Official Social Impact & Rural Development Club.',
details: 'RIT Unnat Bharat Abhiyan (UBA) club focuses on sustainable village development, plantation drives, environmental awareness, and impactful social initiatives across rural Tamil Nadu.',
category: 'Social',
members: 1153,
presidentName: 'UBA Core Lead',
year: 'Final Year',
contactEmail: 'uba@ritchennai.edu.in',
contactPhone: '+91 98765 43218',
icon: 'Users',
logoUrl: '/clubs/unnat_bharat.png',
socialLinks: {
instagram: 'https://www.instagram.com/ritunnatbharatabhiyan/',
},
},
{
id: 'mediastic',
name: 'MEDIASTIC HUB (SOCIAL MEDIA CLUB)',
description: 'Official Social Media & Digital Content Creation Club of Rajalakshmi Institute of Technology.',
details: 'Mediastic Hub handles official video production, campus media coverage, event highlights for RobochipX & Quant-a-thon, photography exhibitions, and RIT digital channels.',
category: 'Creative',
members: 1351,
presidentName: 'Media Hub Lead',
year: 'Final Year',
contactEmail: 'mediastichub@ritchennai.edu.in',
contactPhone: '+91 98765 43219',
icon: 'Camera',
logoUrl: '/clubs/mediastic.png',
socialLinks: {
instagram: 'https://www.instagram.com/mediastichub_rit/',
youtube: 'https://www.youtube.com/@rajalakshmiinstituteoftech444',
},
},
{
id: 'helios',
name: 'HELIOS RIT (PHOTOGRAPHY CLUB)',
description: 'Capturing Life\'s Moments, One Frame at a Time. Showcasing cinematic shots & campus stories.',
details: 'Helios RIT Official is the premier photography and filmmaking club of RIT, capturing campus events, hosting photography contests, guest lectures, and showcasing cinematic stories of RIT.',
category: 'Creative',
members: 2455,
presidentName: 'Photography Lead',
year: 'Final Year',
contactEmail: 'helios@ritchennai.edu.in',
contactPhone: '+91 98765 43220',
icon: 'Camera',
logoUrl: '/clubs/helios.png',
socialLinks: {
instagram: 'https://www.instagram.com/helios.rit/',
},
},
{
id: 'techspark',
name: 'TECHSPARK CLUB RIT',
description: 'Where Tech meets creativity, Sparks fly! Fostering innovation in every byte.',
details: 'TechSpark Club RIT is the technical innovation club driving Web3 Camp Meets AI, GirlScript hackathons, Spark Booths, coding competitions, and hands-on developer projects.',
category: 'Technical',
members: 672,
presidentName: 'TechSpark Lead',
year: 'Third Year',
contactEmail: 'techspark@ritchennai.edu.in',
contactPhone: '+91 98765 43221',
icon: 'Zap',
logoUrl: '/clubs/techspark.png',
socialLinks: {
instagram: 'https://www.instagram.com/techspark.rit/',
whatsapp: 'https://chat.whatsapp.com/I8B5Wdybrew2dYYDrE4PG',
},
},
{
id: 'wec',
name: 'WOMEN EMPOWERMENT CELL (WEC)',
description: 'Empower • Excel • Elevate. Supporting women in technology, leadership & coding.',
details: 'Women Empowerment Cell (WEC RIT) organizes She Shines @ Hexaware coding challenges, Womens Empowerment Week, ZYRA Days, leadership seminars, and career development initiatives.',
category: 'Social',
members: 428,
presidentName: 'Pooja WYA',
year: 'Final Year',
contactEmail: 'wec@ritchennai.edu.in',
contactPhone: '+91 98765 43222',
icon: 'Sparkles',
logoUrl: '/clubs/wec.png',
socialLinks: {
instagram: 'https://www.instagram.com/wec.rit/',
},
},
{
id: 'rotaract',
name: 'ROTARACT CLUB OF RIT (RCRIT)',
description: 'Embrace the joy of giving. Official Rotaract organization for leadership & service.',
details: 'Rotaract Club of RIT (RCRIT) under Rotary International sponsors Bloom mental health seminars, Phoenix fests, RITARIQ 365, Ryla leadership camps, and community blood donation drives.',
category: 'Social',
members: 1813,
presidentName: 'Hari Balaji',
year: 'Final Year',
contactEmail: 'rotaract@ritchennai.edu.in',
contactPhone: '+91 98765 43223',
icon: 'HeartHandshake',
logoUrl: '/clubs/rotaract.png',
socialLinks: {
instagram: 'https://www.instagram.com/rcrit/',
},
},
{
id: 'yuva',
name: 'Yi-CH YUVA RIT',
description: 'Official Page of YUVA Club RIT under Young Indians (Yi). Inspiring youth leadership.',
details: 'Yi-CH YUVA RIT leads Chennai Get Talent, Safe Miles road safety awareness, Blogathons, Tree Plantation drives (500 Trees Mission), and youth leadership conventions.',
category: 'Social',
members: 1460,
presidentName: 'YUVA Student Chair',
year: 'Final Year',
contactEmail: 'yuva@ritchennai.edu.in',
contactPhone: '+91 98765 43224',
icon: 'Rocket',
logoUrl: '/clubs/yuva.png',
socialLinks: {
instagram: 'https://www.instagram.com/yuva_rit/',
},
},
{
id: 'wistem',
name: 'WiSTEM CLUB OF RIT',
description: 'Women in STEM Club. AI Quests, STEM Chronicles, CodeVsClick & maker empowering events.',
details: 'WiSTEM Club of RIT empowers women in Science, Technology, Engineering, and Mathematics through AI Quests, STEM Chronicles newsletter, CodeVsClick contests, and technical workshops.',
category: 'Technical',
members: 453,
presidentName: 'WiSTEM Lead',
year: 'Third Year',
contactEmail: 'wistem@ritchennai.edu.in',
contactPhone: '+91 98765 43225',
icon: 'Atom',
logoUrl: '/clubs/wistem.png',
socialLinks: {
instagram: 'https://www.instagram.com/wistem._.rit/',
website: 'https://www.rityatra.in/',
},
},
{
id: 'steam',
name: 'STEAM CLUB RIT',
description: 'Innovate - Collaborate - Elevate. Hands-on RobochipX, Quant-a-thon & AI/Chip design.',
details: 'STEAM Club RIT focuses on hands-on project creation, RobochipX 24-hour hackathons, Quant-a-thon quantum technology events, chip design, and robotics innovations.',
category: 'Technical',
members: 895,
presidentName: 'STEAM President',
year: 'Final Year',
contactEmail: 'steamclub@ritchennai.edu.in',
contactPhone: '+91 98765 43226',
icon: 'Printer',
logoUrl: '/clubs/steam.png',
socialLinks: {
instagram: 'https://www.instagram.com/steam_club.rit/',
},
},
];
// ─── Community Q&A ───────────────────────────────────────────────────────────
@@ -390,10 +925,11 @@ export const AI_SUGGESTED_PROMPTS = [
export const DEPARTMENTS = [
'All Departments',
'Computer Science & Engineering',
'Electronics & Communication',
'Computer Science & Business Systems',
'Artificial Intelligence & Machine Learning',
'Electronics & Communication Engineering',
'Mechanical Engineering',
'Civil Engineering',
'Information Technology',
'Electrical & Electronics',
'Artificial Intelligence & Data Science',
'Electrical & Electronics Engineering',
];

View File

@@ -157,7 +157,7 @@ export default function AIAssistant() {
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #FB923C)' : '#F8FAFC',
color: msg.role === 'user' ? 'white' : '#1E293B',
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
border: msg.role === 'ai' ? '1px solid #E5E7EB' : 'none',
border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none',
fontFamily: 'Inter, sans-serif',
}}
dangerouslySetInnerHTML={{ __html: formatContent(msg.content) }}

View File

@@ -1,12 +1,13 @@
import { useState } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Search, ChevronRight, MapPin } from 'lucide-react';
import { Search, ChevronRight, MapPin, Users, BookOpen, Filter, ArrowUpDown, Loader2 } from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import BusCard from '@/components/BusCard/BusCard';
import FacultyCard from '@/components/FacultyCard/FacultyCard';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import { BUS_ROUTES, FACULTY_DATA, CAMPUS_LOCATIONS, DEPARTMENTS } from '@/constants';
import { FACULTY_DATA, CAMPUS_LOCATIONS, DEPARTMENTS } from '@/constants';
import * as LucideIcons from 'lucide-react';
import type { BusRoute } from '@/types';
type Tab = 'map' | 'bus' | 'faculty';
@@ -14,16 +15,52 @@ export default function Campus() {
const [activeTab, setActiveTab] = useState<Tab>('map');
const [searchFaculty, setSearchFaculty] = useState('');
const [selectedDept, setSelectedDept] = useState('All Departments');
const [sortBy, setSortBy] = useState('Name A-Z');
const [busSearch, setBusSearch] = useState('');
const [busRoutes, setBusRoutes] = useState<BusRoute[]>([]);
const [loadingBus, setLoadingBus] = useState(true);
useEffect(() => {
if (activeTab === 'bus' && busRoutes.length === 0) {
setLoadingBus(true);
fetch('http://localhost:8080/api/bus-routes')
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then((data) => {
setBusRoutes(data);
setLoadingBus(false);
})
.catch((err) => {
console.error('Error fetching bus routes:', err);
setLoadingBus(false);
});
}
}, [activeTab, busRoutes.length]);
const filteredAndSortedFaculty = useMemo(() => {
const filtered = FACULTY_DATA.filter((f) => {
const searchLower = searchFaculty.trim().toLowerCase();
const matchSearch =
f.name.toLowerCase().includes(searchLower) ||
f.department.toLowerCase().includes(searchLower) ||
f.designation.toLowerCase().includes(searchLower) ||
(f.specialization && f.specialization.toLowerCase().includes(searchLower));
const filteredFaculty = FACULTY_DATA.filter((f) => {
const matchSearch = f.name.toLowerCase().includes(searchFaculty.toLowerCase()) ||
f.department.toLowerCase().includes(searchFaculty.toLowerCase());
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
return matchSearch && matchDept;
});
const filteredRoutes = BUS_ROUTES.filter((r) =>
return filtered.sort((a, b) => {
if (sortBy === 'Name A-Z') return a.name.localeCompare(b.name);
if (sortBy === 'Department') return a.department.localeCompare(b.department);
if (sortBy === 'Designation') return a.designation.localeCompare(b.designation);
return 0;
});
}, [searchFaculty, selectedDept, sortBy]);
const filteredRoutes = busRoutes.filter((r) =>
r.name.toLowerCase().includes(busSearch.toLowerCase()) ||
r.from.toLowerCase().includes(busSearch.toLowerCase()) ||
r.number.toLowerCase().includes(busSearch.toLowerCase())
@@ -36,7 +73,17 @@ export default function Campus() {
];
return (
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
<div className="min-h-screen relative overflow-hidden" style={{ backgroundColor: activeTab === 'faculty' ? '#F8FAFC' : '#FAFAFA' }}>
{/* Background decorations for Faculty tab */}
{activeTab === 'faculty' && (
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute top-0 left-0 w-full h-96 bg-gradient-to-b from-white/80 to-transparent" />
<div className="absolute top-20 -left-64 w-[500px] h-[500px] bg-purple-200/40 rounded-full blur-3xl opacity-50" />
<div className="absolute top-40 -right-64 w-[600px] h-[600px] bg-orange-200/40 rounded-full blur-3xl opacity-40" />
<div className="absolute inset-0" style={{ backgroundImage: 'radial-gradient(rgba(148, 163, 184, 0.1) 2px, transparent 2px)', backgroundSize: '32px 32px' }} />
</div>
)}
<div className="relative z-10">
{/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="container-custom">
@@ -61,7 +108,7 @@ export default function Campus() {
{/* Tabs */}
<div className="flex gap-2 bg-white rounded-2xl p-2 border border-[#E5E7EB] mb-8 w-fit" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
{TABS.map((tab) => {
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[tab.icon];
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[tab.icon];
return (
<button
key={tab.id}
@@ -144,7 +191,7 @@ export default function Campus() {
{/* Quick Nav */}
<div className="p-6 grid grid-cols-4 sm:grid-cols-8 gap-4">
{CAMPUS_LOCATIONS.map((loc) => {
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
return (
<motion.button
key={loc.id}
@@ -179,6 +226,21 @@ export default function Campus() {
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
{loadingBus ? (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<Loader2 className="w-8 h-8 text-[#F97316] animate-spin" />
<p className="text-sm text-[#94A3B8]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Loading live bus routes from RIT Transport...
</p>
</div>
) : filteredRoutes.length === 0 ? (
<div className="text-center py-16 text-[#94A3B8]">
<p className="text-sm font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>
No bus routes found matching "{busSearch}"
</p>
</div>
) : (
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{filteredRoutes.map((route) => (
<StaggerItem key={route.id}>
@@ -186,45 +248,134 @@ export default function Campus() {
</StaggerItem>
))}
</StaggerContainer>
)}
</motion.div>
)}
{/* Faculty Tab */}
{activeTab === 'faculty' && (
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} className="space-y-8">
{/* Faculty Hero Section */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
<div className="lg:col-span-5 bg-white rounded-3xl p-8 border border-[#E8ECF4] shadow-sm flex flex-col justify-center relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-bl-full -z-10" />
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#FF7A00] to-[#FB923C] flex items-center justify-center text-white mb-6 shadow-md">
<Users className="w-7 h-7" />
</div>
<h2 className="text-3xl font-bold text-[#1E293B] mb-3" style={{ fontFamily: 'Poppins, sans-serif' }}>
Faculty Directory
</h2>
<p className="text-[#64748B] text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
Browse and connect with experienced faculty members. Search by name, department, or specialization.
</p>
</div>
<div className="lg:col-span-7 bg-white rounded-3xl p-6 border border-[#E8ECF4] shadow-sm">
<h3 className="text-sm font-semibold text-[#1E293B] mb-4 uppercase tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>Department Overview</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ abrv: 'CSE', full: 'Computer Science & Engineering', color: 'blue' },
{ abrv: 'CSBS', full: 'Computer Science & Business Systems', color: 'indigo' },
{ abrv: 'AIML', full: 'Artificial Intelligence & Machine Learning', color: 'sky' },
{ abrv: 'ECE', full: 'Electronics & Communication Engineering', color: 'purple' },
{ abrv: 'MECH', full: 'Mechanical Engineering', color: 'orange' },
{ abrv: 'CIVIL', full: 'Civil Engineering', color: 'emerald' },
{ abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science', color: 'pink' },
{ abrv: 'EEE', full: 'Electrical & Electronics Engineering', color: 'yellow' },
].map(d => {
const count = FACULTY_DATA.filter(f => f.department === d.full).length;
return (
<div key={d.abrv} className={`p-4 rounded-2xl bg-${d.color}-50 border border-${d.color}-100 flex flex-col gap-1 transition-transform hover:scale-105 cursor-default`}>
<div className="flex justify-between items-center">
<span className={`text-xs font-bold text-${d.color}-700`} style={{ fontFamily: 'Poppins, sans-serif' }}>{d.abrv}</span>
<BookOpen className={`w-3.5 h-3.5 text-${d.color}-400`} />
</div>
<span className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>{count}</span>
</div>
);
})}
</div>
</div>
</div>
{/* Search Section */}
<div className="bg-white rounded-3xl p-4 border border-[#E8ECF4] shadow-sm flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-[#94A3B8]" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#94A3B8]" />
<input
type="text"
placeholder="Search faculty by name or department..."
placeholder="Search by faculty name, department, designation or specialization..."
value={searchFaculty}
onChange={(e) => setSearchFaculty(e.target.value)}
className="w-full pl-11 pr-4 py-3 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] transition-colors"
className="w-full pl-12 pr-4 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative">
<Filter className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94A3B8] pointer-events-none" />
<select
value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)}
className="px-4 py-3 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#475569] focus:outline-none focus:border-[#F97316] transition-colors"
style={{ fontFamily: 'Inter, sans-serif' }}
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
>
{DEPARTMENTS.map((dept) => (
<option key={dept} value={dept}>{dept}</option>
))}
{DEPARTMENTS.map((dept) => {
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
return <option key={dept} value={dept}>{dept} ({count})</option>;
})}
</select>
</div>
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredFaculty.map((faculty) => (
<div className="relative">
<ArrowUpDown className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94A3B8] pointer-events-none" />
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer"
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
>
<option value="Name A-Z">Sort: Name A-Z</option>
<option value="Department">Sort: Department</option>
<option value="Designation">Sort: Designation</option>
</select>
</div>
</div>
</div>
{/* Grid */}
{filteredAndSortedFaculty.length > 0 ? (
<StaggerContainer key={`${searchFaculty}-${selectedDept}-${sortBy}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-20">
{filteredAndSortedFaculty.map((faculty) => (
<StaggerItem key={faculty.id}>
<FacultyCard faculty={faculty} />
</StaggerItem>
))}
</StaggerContainer>
) : (
<div className="py-20 text-center flex flex-col items-center justify-center bg-white rounded-3xl border border-[#E8ECF4] shadow-sm">
<Users className="w-12 h-12 text-[#CBD5E1] mb-4" />
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>No faculty found</h3>
<p className="text-[#64748B] text-sm mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>Try adjusting your search or filters.</p>
</div>
)}
{/* Bottom Status */}
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2"
>
<span className="text-lg">👥</span>
<span className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
Showing {filteredAndSortedFaculty.length} Faculty Members
</span>
</motion.div>
</div>
</motion.div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,31 +1,157 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
MessageCircle, Heart, TrendingUp, ThumbsUp, Send,
Shield, Lock, Smile, ChevronRight, Search, Plus
Shield, Lock, Smile, ChevronRight, Search, Plus, User
} from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { QUESTIONS_DATA, CONFESSIONS_DATA } from '@/constants';
import { formatDate } from '@/lib/utils';
type Tab = 'qa' | 'confession';
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library'];
const AVATARS: Record<string, string> = {
'Priya S.': 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&h=120&q=80',
'Ravi K.': 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&h=120&q=80',
'Arun M.': 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=120&h=120&q=80',
};
const getAvatar = (author: string) => {
return AVATARS[author] || `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(author)}`;
};
const getRelativeTime = (dateString: string) => {
if (!dateString) return 'just now';
// Normalize microsecond timestamps (e.g. 2026-07-22T13:28:18.174367) to standard millisecond precision
let normalized = dateString;
const dotIndex = dateString.indexOf('.');
if (dotIndex !== -1) {
const mainPart = dateString.substring(0, dotIndex);
let msPart = dateString.substring(dotIndex + 1);
// Strip any trailing non-digits (like timezone offsets Z or +05:30) for truncation, then keep first 3 digits
const nonDigitMatch = msPart.match(/\D/);
let suffix = '';
if (nonDigitMatch && nonDigitMatch.index !== undefined) {
suffix = msPart.substring(nonDigitMatch.index);
msPart = msPart.substring(0, nonDigitMatch.index);
}
normalized = `${mainPart}.${msPart.substring(0, 3)}${suffix}`;
}
const now = new Date().getTime(); // Dynamic local time
const past = new Date(normalized).getTime();
if (isNaN(past)) return 'just now';
const msPerMinute = 60 * 1000;
const msPerHour = msPerMinute * 60;
const msPerDay = msPerHour * 24;
const elapsed = now - past;
if (elapsed < msPerMinute) {
return 'just now';
} else if (elapsed < msPerHour) {
return Math.round(elapsed / msPerMinute) + 'm ago';
} else if (elapsed < msPerDay) {
return Math.round(elapsed / msPerHour) + 'h ago';
} else {
const days = Math.round(elapsed / msPerDay);
return days === 1 ? 'yesterday' : `${days}d ago`;
}
};
export default function Community() {
const PAGE_SIZE = 5;
const [activeTab, setActiveTab] = useState<Tab>('qa');
const [confessionText, setConfessionText] = useState('');
const [questionText, setQuestionText] = useState('');
const [authorName, setAuthorName] = useState('');
const [confessionPosted, setConfessionPosted] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [likedIds, setLikedIds] = useState<Set<string>>(new Set());
const [expandedQuestionIds, setExpandedQuestionIds] = useState<Set<string>>(new Set());
// Server-side pagination
const [currentPage, setCurrentPage] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [questions, setQuestions] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// Fallback static questions for when backend is unavailable
const [staticQuestions] = useState<any[]>(QUESTIONS_DATA);
const filteredQuestions = QUESTIONS_DATA.filter((q) =>
q.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
q.tags.some((t) => t.includes(searchQuery.toLowerCase()))
);
const toggleAnswers = (id: string) => {
setExpandedQuestionIds((prev) => {
const next = new Set(prev);
if (next.has(id.toString())) {
next.delete(id.toString());
} else {
next.add(id.toString());
}
return next;
});
};
const fetchQuestions = async (page = 0) => {
setLoading(true);
try {
const response = await fetch(`http://localhost:8080/api/questions/paged?page=${page}&size=${PAGE_SIZE}`);
if (response.ok) {
const data = await response.json();
// Spring Page response: { content: [], totalPages, totalElements, number }
setQuestions(data.content || []);
setTotalPages(data.totalPages || 1);
setCurrentPage(data.number ?? page);
} else {
console.error("Failed to fetch questions from backend: HTTP status", response.status);
// Fallback: slice static questions
const start = page * PAGE_SIZE;
setQuestions(staticQuestions.slice(start, start + PAGE_SIZE));
setTotalPages(Math.ceil(staticQuestions.length / PAGE_SIZE));
setCurrentPage(page);
}
} catch (error) {
console.error('Backend not available. Falling back to local static questions data.', error);
const start = page * PAGE_SIZE;
setQuestions(staticQuestions.slice(start, start + PAGE_SIZE));
setTotalPages(Math.ceil(staticQuestions.length / PAGE_SIZE));
setCurrentPage(page);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchQuestions(0);
}, []);
const getAnswersCount = (q: any) => {
if (Array.isArray(q.answers)) return q.answers.length;
if (typeof q.answers === 'number') return q.answers;
return 0;
};
const getVotesCount = (q: any) => {
const votesVal = typeof q.upvotes === 'number' ? q.upvotes : (typeof q.votes === 'number' ? q.votes : 0);
return votesVal;
};
// Client-side filter applied on top of current page (for search within page)
const filteredQuestions = questions.filter((q) => {
const titleVal = q.title || "";
const tagsVal = q.tags || [];
return titleVal.toLowerCase().includes(searchQuery.toLowerCase()) ||
tagsVal.some((t: string) => t.toLowerCase().includes(searchQuery.toLowerCase()));
});
const handlePageChange = (newPage: number) => {
setExpandedQuestionIds(new Set()); // collapse any open answers
fetchQuestions(newPage);
// Scroll back to the top of the questions list smoothly
document.getElementById('qa-questions-list')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const handleConfess = () => {
if (!confessionText.trim()) return;
@@ -34,34 +160,90 @@ export default function Community() {
setTimeout(() => setConfessionPosted(false), 3000);
};
const toggleLike = (id: string) => {
const toggleLike = async (id: string) => {
setLikedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
try {
await fetch(`http://localhost:8080/api/questions/${id}/upvote`, {
method: 'POST'
});
fetchQuestions();
} catch (error) {
console.log('Backend not available for upvote sync.', error);
}
};
const handlePostQuestion = async () => {
if (!questionText.trim()) return;
const displayAuthor = authorName.trim() || 'Anonymous';
const newQuestion = {
title: questionText.split('\n')[0].substring(0, 100) || "Q&A Question",
body: questionText,
author: displayAuthor,
tags: ['fresher', 'general'],
upvotes: 0,
isAnswered: false,
createdAt: new Date().toISOString(),
answers: []
};
try {
const response = await fetch('http://localhost:8080/api/questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: newQuestion.title,
body: newQuestion.body,
author: newQuestion.author,
tags: newQuestion.tags
})
});
if (response.ok) {
const saved = await response.json();
setQuestions(prev => {
// Prepend saved question and filter out duplicate placeholders
const filtered = prev.filter(q => q.id.toString() !== saved.id.toString());
return [saved, ...filtered];
});
setQuestionText('');
setAuthorName('');
} else {
setQuestions(prev => [{ ...newQuestion, id: String(Date.now()) }, ...prev]);
setQuestionText('');
setAuthorName('');
}
} catch (error) {
console.error("Error saving question:", error);
setQuestions(prev => [{ ...newQuestion, id: String(Date.now()) }, ...prev]);
setQuestionText('');
setAuthorName('');
}
};
return (
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
<div className="min-h-screen" style={{ backgroundColor: '#FAFBFD' }}>
{/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="bg-white border-b border-slate-100 py-12">
<div className="container-custom">
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
RIT{' '}
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
Community
</span>
<h1 className="text-2xl md:text-3xl font-bold text-slate-900 mb-2 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
RIT Community
</h1>
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
<p className="text-slate-500 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
Ask questions, share confessions, and connect with fellow RIT students.
</p>
</div>
</div>
<div className="container-custom py-8">
<div className="container-custom py-10">
{/* Tabs */}
<div className="flex gap-2 bg-white rounded-2xl p-2 border border-[#E5E7EB] mb-8 w-fit" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
<div className="flex gap-1.5 bg-slate-100 rounded-xl p-1.5 border border-slate-200/40 mb-8 w-fit">
{[
{ id: 'qa' as Tab, label: 'Freshers Q&A', icon: MessageCircle },
{ id: 'confession' as Tab, label: 'Confessions', icon: Heart },
@@ -69,19 +251,18 @@ export default function Community() {
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className="relative flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-medium transition-all"
style={{ fontFamily: 'Poppins, sans-serif', color: activeTab === tab.id ? 'white' : '#475569' }}
className="relative flex items-center gap-2 px-4 py-2 rounded-lg text-[13px] font-medium transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif', color: activeTab === tab.id ? '#ffffff' : '#64748B' }}
>
{activeTab === tab.id && (
<motion.div
layoutId="community-tab"
className="absolute inset-0 rounded-xl"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
transition={{ type: 'spring', bounce: 0.2, duration: 0.4 }}
className="absolute inset-0 rounded-lg bg-slate-950"
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
/>
)}
<span className="relative z-10 flex items-center gap-2">
<tab.icon className="w-4 h-4" />
<tab.icon className="w-3.5 h-3.5" />
{tab.label}
</span>
</button>
@@ -93,33 +274,42 @@ export default function Community() {
{activeTab === 'qa' && (
<motion.div
key="qa"
initial={{ opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.25 }}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
{/* Ask Question Box */}
<div className="bg-white rounded-2xl border border-[#E5E7EB] p-5 mb-6" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
<h3 className="text-sm font-semibold text-[#1E293B] mb-3" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
<div className="bg-white rounded-xl border border-slate-200/80 p-5 mb-8 shadow-xs">
<h3 className="text-sm font-semibold text-slate-900 mb-3 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
<textarea
value={questionText}
onChange={(e) => setQuestionText(e.target.value)}
placeholder="What's on your mind? Ask your seniors anything about RIT..."
rows={3}
className="w-full border border-[#E5E7EB] rounded-xl p-3 text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] resize-none transition-colors mb-3"
className="w-full border border-slate-200 rounded-xl p-3 text-[13px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-slate-400 focus:bg-white bg-slate-50/30 resize-none transition-all mb-3"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
placeholder="Your Name (optional)"
className="w-full md:w-64 border border-slate-200 rounded-xl px-3 py-2 text-[13px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-slate-400 focus:bg-white bg-slate-50/30 transition-all mb-4"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
<div className="flex items-center justify-between">
<span className="text-xs text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>
<span className="text-[11px] text-slate-400" style={{ fontFamily: 'Inter, sans-serif' }}>
Your question will be visible to all students
</span>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white text-sm font-semibold"
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
onClick={handlePostQuestion}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl text-white text-[13px] font-semibold bg-[#F97316] hover:bg-[#EA580C] transition-colors cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<Plus className="w-4 h-4" />
Post Question
@@ -127,92 +317,237 @@ export default function Community() {
</div>
</div>
{/* Search */}
<div className="relative mb-5">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94A3B8]" />
<div className="relative mb-6">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] transition-colors"
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 bg-white text-[13px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-slate-400 transition-colors"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
{/* Questions */}
<StaggerContainer className="flex flex-col gap-4">
{filteredQuestions.map((q) => (
<div id="qa-questions-list">
{loading ? (
<div className="flex flex-col gap-3">
{[...Array(PAGE_SIZE)].map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-slate-200/50 p-5 animate-pulse">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-slate-100 shrink-0" />
<div className="flex-1 space-y-2">
<div className="h-3 w-1/4 rounded bg-slate-100" />
<div className="h-4 w-2/3 rounded bg-slate-100" />
<div className="h-3 w-full rounded bg-slate-100" />
</div>
</div>
</div>
))}
</div>
) : (
<StaggerContainer key={`${currentPage}-${filteredQuestions.length}`} className="flex flex-col gap-4">
{filteredQuestions.map((q, idx) => {
const isFeatured = idx === 0 && searchQuery === '' && currentPage === 0;
return (
<StaggerItem key={q.id}>
<motion.div
whileHover={{ y: -2 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
whileHover={{ y: -0.5 }}
className={`bg-white rounded-xl border p-5 transition-all duration-300 hover:border-slate-350 hover:shadow-[0_8px_30px_rgba(17,24,39,0.015)] ${
isFeatured
? 'border-l-2 border-l-slate-800 border-slate-200'
: 'border-slate-200/50'
}`}
>
<div className="flex items-start gap-4">
{/* Votes */}
<div className="flex flex-col items-center gap-1 shrink-0">
<button
onClick={() => toggleLike(q.id)}
className="w-8 h-8 rounded-lg flex items-center justify-center transition-all"
style={{ backgroundColor: likedIds.has(q.id) ? '#FFF7ED' : '#F8FAFC' }}
>
<ThumbsUp className="w-4 h-4" style={{ color: likedIds.has(q.id) ? '#F97316' : '#94A3B8' }} />
</button>
<span className="text-xs font-semibold text-[#475569]">{q.votes + (likedIds.has(q.id) ? 1 : 0)}</span>
{/* Avatar */}
<img
src={getAvatar(q.author)}
alt={q.author}
className="w-10 h-10 rounded-xl object-cover bg-slate-50 border border-slate-100 shrink-0"
/>
<div className="flex-1 min-w-0">
{/* Header / Meta */}
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className="text-xs font-bold text-slate-800 hover:text-slate-900 transition-colors" style={{ fontFamily: 'Poppins, sans-serif' }}>{q.author}</span>
<span className="text-[10px] text-slate-300"></span>
<span className="text-[10px] text-slate-400 font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>{getRelativeTime(q.createdAt)}</span>
{isFeatured && (
<span className="ml-auto bg-amber-50 text-amber-700 border border-amber-200/40 text-[9px] px-1.5 py-0.5 rounded font-semibold tracking-wide uppercase"> Popular</span>
)}
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-[#1E293B] mb-1.5 hover:text-[#F97316] cursor-pointer transition-colors"
<h3 className={`font-bold text-slate-900 mb-1 hover:text-slate-700 cursor-pointer transition-colors tracking-tight ${
isFeatured ? 'text-base md:text-lg' : 'text-sm md:text-base'
}`}
style={{ fontFamily: 'Poppins, sans-serif' }}>
{q.title}
</h3>
<p className="text-xs text-[#94A3B8] mb-3 line-clamp-2" style={{ fontFamily: 'Inter, sans-serif' }}>{q.body}</p>
<div className="flex items-center justify-between flex-wrap gap-2">
<p className="text-[13px] text-slate-650 mb-4 leading-relaxed line-clamp-2" style={{ fontFamily: 'Inter, sans-serif' }}>
{q.body}
</p>
<div className="flex items-center justify-between flex-wrap gap-3 pt-1.5 border-t border-slate-100/60">
<div className="flex flex-wrap gap-1.5">
{q.tags.map((tag) => (
{(q.tags || []).map((tag: string) => (
<span
key={tag}
className="px-2 py-0.5 rounded-full text-[10px] font-medium bg-[#F8FAFC] text-[#94A3B8] border border-[#E5E7EB]"
className="px-2 py-0.5 rounded-md text-[10px] font-medium bg-slate-50 text-slate-500 border border-slate-100/60 hover:bg-slate-100/60 hover:text-slate-700 transition-colors cursor-pointer"
style={{ fontFamily: 'Inter, sans-serif' }}
>
#{tag}
</span>
))}
</div>
<div className="flex items-center gap-3 text-xs text-[#94A3B8]">
<span className="flex items-center gap-1">
<div className="flex items-center gap-2 text-[10px] text-slate-400 font-bold">
<button
onClick={() => toggleAnswers(q.id)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg transition-all duration-200 cursor-pointer border border-slate-200/30 ${
expandedQuestionIds.has(q.id.toString())
? 'bg-slate-900 text-white border-slate-900'
: 'bg-slate-50 hover:bg-slate-100 text-slate-500 hover:text-slate-700'
}`}
>
<MessageCircle className="w-3.5 h-3.5" />
{q.answers} answers
</span>
<span>{getAnswersCount(q)} answers</span>
</button>
{q.isAnswered && (
<span className="px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-600 text-[10px] font-semibold"> Answered</span>
<span className="bg-emerald-50 text-emerald-700 border border-emerald-200/40 text-[9px] px-1.5 py-0.5 rounded font-semibold tracking-wide uppercase"> Answered</span>
)}
{/* Like/Vote Button inside bottom bar */}
<button
onClick={() => toggleLike(q.id)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg transition-all duration-200 cursor-pointer border ${
likedIds.has(q.id.toString())
? 'bg-rose-50 text-rose-600 border-rose-100/60 hover:bg-rose-100/60'
: 'bg-slate-50 hover:bg-slate-100 text-slate-500 hover:text-slate-700 border-slate-200/30'
}`}
>
<ThumbsUp className="w-3 h-3" />
<span>{getVotesCount(q) + (likedIds.has(q.id.toString()) ? 1 : 0)}</span>
</button>
</div>
</div>
{/* Expandable Answers Section */}
{expandedQuestionIds.has(q.id.toString()) && (
<div className="mt-4 pt-4 border-t border-slate-100 flex flex-col gap-3.5 pl-3 border-l-2 border-l-slate-100">
<div className="flex items-center justify-between">
<h4 className="text-[11px] font-bold text-slate-700 tracking-tight uppercase" style={{ fontFamily: 'Poppins, sans-serif' }}>
Answers ({getAnswersCount(q)})
</h4>
</div>
{Array.isArray(q.answers) && q.answers.length > 0 ? (
<div className="flex flex-col gap-2.5 max-h-60 overflow-y-auto pr-1">
{q.answers.map((ans: any) => (
<div key={ans.id} className="bg-slate-50/50 rounded-xl p-3.5 border border-slate-100 hover:bg-slate-50/80 transition-colors">
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className="text-xs font-bold text-slate-700" style={{ fontFamily: 'Poppins, sans-serif' }}>{ans.author}</span>
<span className="text-[9px] text-slate-300"></span>
<span className="text-[10px] text-slate-400 font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>{getRelativeTime(ans.createdAt)}</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
{ans.body}
</p>
</div>
))}
</div>
) : (
<p className="text-xs text-slate-400 italic font-medium py-1" style={{ fontFamily: 'Inter, sans-serif' }}>
No answers posted yet. Senior helpers can reply to this question via the Telegram Bot!
</p>
)}
</div>
</div>
)}
</div>
</div>
</motion.div>
</StaggerItem>
))}
);
})}
</StaggerContainer>
)}
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-6 px-1">
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.95 }}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 0 || loading}
className={`flex items-center gap-1.5 px-4 py-2 rounded-xl border text-[12px] font-semibold transition-all duration-200 cursor-pointer ${
currentPage === 0 || loading
? 'border-slate-100 bg-slate-50 text-slate-300 cursor-not-allowed'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:border-slate-300 shadow-sm'
}`}
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<ChevronRight className="w-4 h-4 rotate-180" />
Prev
</motion.button>
<div className="flex items-center gap-1.5">
{Array.from({ length: totalPages }).map((_, i) => (
<motion.button
key={i}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => handlePageChange(i)}
disabled={loading}
className={`w-7 h-7 rounded-lg text-[11px] font-bold transition-all duration-200 cursor-pointer border ${
i === currentPage
? 'bg-slate-900 text-white border-slate-900'
: 'bg-white text-slate-500 border-slate-200 hover:border-slate-300 hover:bg-slate-50'
}`}
style={{ fontFamily: 'Poppins, sans-serif' }}
>
{i + 1}
</motion.button>
))}
</div>
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.95 }}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages - 1 || loading}
className={`flex items-center gap-1.5 px-4 py-2 rounded-xl border text-[12px] font-semibold transition-all duration-200 cursor-pointer ${
currentPage >= totalPages - 1 || loading
? 'border-slate-100 bg-slate-50 text-slate-300 cursor-not-allowed'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:border-slate-300 shadow-sm'
}`}
style={{ fontFamily: 'Poppins, sans-serif' }}
>
Next
<ChevronRight className="w-4 h-4" />
</motion.button>
</div>
)}
</div>
</div>
{/* Sidebar */}
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-6">
{/* Trending */}
<AnimatedContainer direction="right" delay={0.1}>
<div className="bg-white rounded-2xl border border-[#E5E7EB] p-5" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
<div className="bg-white rounded-xl border border-slate-200/80 p-5 shadow-xs">
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-4.5 h-4.5 text-[#F97316]" />
<h3 className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Trending Topics</h3>
<TrendingUp className="w-4 h-4 text-slate-600" />
<h3 className="text-sm font-semibold text-slate-850 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Trending Topics</h3>
</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-1.5">
{TRENDING_TAGS.map((tag) => (
<button
key={tag}
onClick={() => setSearchQuery(tag)}
className="px-3 py-1.5 rounded-full text-xs font-medium bg-[#FFF7ED] text-[#F97316] hover:bg-[#F97316] hover:text-white transition-all border border-[#FED7AA]"
className="px-2.5 py-1.5 rounded-lg text-[11px] font-medium bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-900 transition-all border-0 cursor-pointer"
style={{ fontFamily: 'Inter, sans-serif' }}
>
#{tag}
@@ -224,15 +559,15 @@ export default function Community() {
{/* Stats */}
<AnimatedContainer direction="right" delay={0.2}>
<div className="bg-white rounded-2xl border border-[#E5E7EB] p-5" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
<div className="bg-white rounded-xl border border-slate-200/80 p-5 shadow-xs">
{[
{ label: 'Total Questions', value: '124' },
{ label: 'Answered', value: '98%' },
{ label: 'Active Students', value: '340+' },
].map((stat, i) => (
<div key={i} className={`flex justify-between py-2.5 ${i < 2 ? 'border-b border-[#E5E7EB]' : ''}`}>
<span className="text-xs text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>{stat.label}</span>
<span className="text-xs font-bold text-[#F97316]" style={{ fontFamily: 'Poppins, sans-serif' }}>{stat.value}</span>
<div key={i} className={`flex justify-between py-2.5 ${i < 2 ? 'border-b border-slate-100' : ''}`}>
<span className="text-xs text-slate-400" style={{ fontFamily: 'Inter, sans-serif' }}>{stat.label}</span>
<span className="text-xs font-bold text-slate-900" style={{ fontFamily: 'Poppins, sans-serif' }}>{stat.value}</span>
</div>
))}
</div>
@@ -246,25 +581,25 @@ export default function Community() {
{activeTab === 'confession' && (
<motion.div
key="confession"
initial={{ opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.25 }}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
{/* Post confession */}
<div
className="rounded-3xl p-6 mb-8 border"
style={{ background: 'linear-gradient(135deg, #1E293B, #334155)', borderColor: 'rgba(255,255,255,0.1)' }}
className="rounded-xl p-5 mb-6 border"
style={{ background: '#0F172A', borderColor: 'rgba(255,255,255,0.05)' }}
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: 'rgba(249,115,22,0.2)' }}>
<Lock className="w-5 h-5 text-[#F97316]" />
<div className="w-9 h-9 rounded-lg flex items-center justify-center bg-slate-800">
<Lock className="w-4.5 h-4.5 text-slate-300" />
</div>
<div>
<h3 className="text-white font-semibold text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>Share Anonymously</h3>
<p className="text-slate-400 text-xs">Your identity is never revealed</p>
<h3 className="text-white font-semibold text-sm tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Share Anonymously</h3>
<p className="text-slate-400 text-[11px]">Your identity is never revealed</p>
</div>
</div>
@@ -273,29 +608,26 @@ export default function Community() {
onChange={(e) => setConfessionText(e.target.value)}
placeholder="Share your thoughts, stories, crushes, or anything on your mind... It's completely anonymous 🤫"
rows={4}
className="w-full rounded-xl p-4 text-sm placeholder-slate-500 focus:outline-none resize-none mb-4"
className="w-full rounded-xl p-3 text-[13px] placeholder-slate-500 focus:outline-none resize-none mb-4 bg-white/5 border border-white/10 text-white"
style={{
fontFamily: 'Inter, sans-serif',
backgroundColor: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.1)',
color: 'white',
}}
/>
<div className="flex items-center gap-3 p-3 rounded-xl mb-4" style={{ backgroundColor: 'rgba(249,115,22,0.1)', border: '1px solid rgba(249,115,22,0.2)' }}>
<Shield className="w-4 h-4 text-[#F97316] shrink-0" />
<span className="text-xs text-slate-400" style={{ fontFamily: 'Inter, sans-serif' }}>
<div className="flex items-center gap-2.5 p-3 rounded-lg mb-4 bg-white/5 border border-white/10">
<Shield className="w-4 h-4 text-slate-300 shrink-0" />
<span className="text-[11px] text-slate-400" style={{ fontFamily: 'Inter, sans-serif' }}>
No IP tracking. No username. 100% anonymous posting.
</span>
</div>
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
onClick={handleConfess}
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-white text-sm font-semibold"
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl text-white text-[13px] font-semibold bg-[#F97316] hover:bg-[#EA580C] transition-colors cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<Smile className="w-4 h-4" />
Post Anonymously
@@ -322,39 +654,41 @@ export default function Community() {
{CONFESSIONS_DATA.map((conf) => (
<StaggerItem key={conf.id}>
<motion.div
whileHover={{ y: -2 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
whileHover={{ y: -0.5 }}
className="bg-white rounded-xl border border-slate-200/50 p-5 transition-all duration-300 hover:border-slate-350 hover:shadow-[0_8px_30px_rgba(17,24,39,0.015)]"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-xl flex items-center justify-center bg-[#FFF7ED]">
<Shield className="w-4 h-4 text-[#F97316]" />
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-slate-50 border border-slate-100">
<Shield className="w-4 h-4 text-slate-500" />
</div>
<div>
<span className="text-xs font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Anonymous</span>
<p className="text-[10px] text-[#94A3B8]">{formatDate(conf.createdAt)}</p>
<span className="text-xs font-bold text-slate-800" style={{ fontFamily: 'Poppins, sans-serif' }}>Anonymous</span>
<p className="text-[10px] text-slate-400 font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>{getRelativeTime(conf.createdAt)}</p>
</div>
</div>
{conf.category && (
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-semibold bg-[#FFF7ED] text-[#F97316] border border-[#FED7AA]" style={{ fontFamily: 'Poppins, sans-serif' }}>
<span className="px-2 py-0.5 rounded text-[10px] font-semibold bg-slate-55 text-slate-650 border border-slate-200/40" style={{ fontFamily: 'Poppins, sans-serif' }}>
{conf.category}
</span>
)}
</div>
<p className="text-sm text-[#475569] leading-relaxed mb-4" style={{ fontFamily: 'Inter, sans-serif' }}>{conf.content}</p>
<p className="text-[13px] text-slate-750 leading-relaxed mb-4" style={{ fontFamily: 'Inter, sans-serif' }}>{conf.content}</p>
<div className="flex items-center justify-between">
<button
onClick={() => toggleLike(conf.id)}
className="flex items-center gap-2 px-3 py-1.5 rounded-xl transition-all"
style={{ backgroundColor: likedIds.has(conf.id) ? '#FFF7ED' : '#F8FAFC' }}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg transition-all duration-200 cursor-pointer border text-[10px] font-bold ${
likedIds.has(conf.id)
? 'bg-rose-50 text-rose-600 border-rose-100/60 hover:bg-rose-100/60'
: 'bg-slate-50 hover:bg-slate-100 text-slate-500 hover:text-slate-700 border-slate-200/30'
}`}
>
<Heart
className="w-3.5 h-3.5"
style={{ color: likedIds.has(conf.id) ? '#F97316' : '#94A3B8' }}
fill={likedIds.has(conf.id) ? '#F97316' : 'none'}
fill={likedIds.has(conf.id) ? '#e11d48' : 'none'}
color={likedIds.has(conf.id) ? '#e11d48' : 'currentColor'}
/>
<span className="text-xs text-[#94A3B8]">{conf.reactions + (likedIds.has(conf.id) ? 1 : 0)}</span>
<span>{conf.reactions + (likedIds.has(conf.id) ? 1 : 0)}</span>
</button>
</div>
</motion.div>
@@ -365,9 +699,9 @@ export default function Community() {
{/* Sidebar */}
<AnimatedContainer direction="right" delay={0.15}>
<div className="bg-white rounded-2xl border border-[#E5E7EB] p-5" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
<h3 className="text-sm font-semibold text-[#1E293B] mb-4 flex items-center gap-2" style={{ fontFamily: 'Poppins, sans-serif' }}>
<Shield className="w-4 h-4 text-[#F97316]" />
<div className="bg-white rounded-xl border border-slate-200/80 p-5 shadow-xs">
<h3 className="text-sm font-semibold text-slate-850 mb-4 flex items-center gap-2" style={{ fontFamily: 'Poppins, sans-serif' }}>
<Shield className="w-4 h-4 text-slate-500" />
Community Rules
</h3>
{[
@@ -377,9 +711,9 @@ export default function Community() {
'Keep it relevant to campus life',
'Confessions are 100% anonymous',
].map((rule, i) => (
<div key={i} className="flex items-start gap-2.5 py-2.5 border-b border-[#E5E7EB] last:border-0">
<ChevronRight className="w-3.5 h-3.5 text-[#F97316] mt-0.5 shrink-0" />
<span className="text-xs text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>{rule}</span>
<div key={i} className="flex items-start gap-2.5 py-2.5 border-b border-slate-100 last:border-0">
<ChevronRight className="w-3.5 h-3.5 text-slate-400 mt-0.5 shrink-0" />
<span className="text-xs text-slate-650" style={{ fontFamily: 'Inter, sans-serif' }}>{rule}</span>
</div>
))}
</div>

View File

@@ -1,18 +1,16 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Calendar, MapPin, Clock, Users, ChevronLeft, ChevronRight, Tag, ExternalLink } from 'lucide-react';
import {
Users, Tag, X, Mail, Phone, UserCheck, GraduationCap, Info,
Atom, Wifi, Printer, Zap, Languages, Calculator, HeartHandshake,
Rocket, Camera, Sparkles, Mic, Target, ArrowRight, RotateCcw,
MessageCircle, ExternalLink, CheckCircle2, Compass, Globe, BookOpen, Heart
} from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { EVENTS_DATA, CLUBS_DATA } from '@/constants';
const CATEGORY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
technical: { bg: '#EFF6FF', text: '#3B82F6', border: '#BFDBFE' },
cultural: { bg: '#FDF2F8', text: '#EC4899', border: '#FBCFE8' },
sports: { bg: '#ECFDF5', text: '#10B981', border: '#A7F3D0' },
seminar: { bg: '#FFF7ED', text: '#F97316', border: '#FED7AA' },
workshop: { bg: '#F5F3FF', text: '#8B5CF6', border: '#DDD6FE' },
};
import { CLUBS_DATA } from '@/constants';
import type { Club } from '@/types';
const CLUB_CATEGORY_COLORS: Record<string, string> = {
Technical: '#3B82F6',
@@ -21,21 +19,173 @@ const CLUB_CATEGORY_COLORS: Record<string, string> = {
Creative: '#F59E0B',
};
const CLUB_ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
Atom,
Wifi,
Printer,
Zap,
Languages,
Calculator,
HeartHandshake,
Rocket,
Camera,
Sparkles,
Mic,
BookOpen,
Globe,
Users,
};
// ─── Inline Brand SVG Icons for Feature 5 ──────────────────────────────────────
const InstagramIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
</svg>
);
const LinkedinIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect width="4" height="12" x="2" y="9" />
<circle cx="4" cy="4" r="2" />
</svg>
);
const YoutubeIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-16.2 0A2 2 0 0 1 2.5 17" />
<polygon points="10 15 15 12 10 9 10 15" fill="currentColor" />
</svg>
);
// ─── Quiz Questions Data for Feature 1 (3-Question Matcher) ─────────────────────
const QUIZ_QUESTIONS = [
{
id: 'interest',
title: '1. What topics or domains are you most interested in?',
options: [
{ label: '🚀 Space Tech, Astronomy & Rocketry', clubIds: ['stellar_space_tech', 'infinitus'] },
{ label: '⚙️ AI Quests, Chip Design, RobochipX & STEM', clubIds: ['steam', 'wistem', 'techspark'] },
{ label: '🎙️ Radio, Podcasting, Storytelling & RJing', clubIds: ['podx', 'mediastic'] },
{ label: '🤝 Community Service, Village Development & NSS Drives', clubIds: ['nss', 'unnat_bharat'] },
{ label: '🎭 Dance, Music, Band & Cultural Arts', clubIds: ['artist_league', 'podx'] },
{ label: '🧮 Fast Calculation, PiDoku, Logic & Mathematics', clubIds: ['infinitus', 'stellar_space_tech'] },
{ label: '✍️ Tamil Literature, Debates & Cultural Heritage', clubIds: ['vaarithi', 'fusion'] },
{ label: '🌸 Japanese Culture, Anime, Manga & Foreign Languages', clubIds: ['nippon', 'fusion'] },
{ label: '📷 Photo/Video Production & Social Media Content', clubIds: ['mediastic', 'helios', 'podx'] },
],
},
{
id: 'skills',
title: '2. What skills do you currently have or want to develop?',
options: [
{ label: '💻 Web3, AI Tools, Circuit Design & Coding Hackathons', clubIds: ['wistem', 'steam', 'techspark'] },
{ label: '🌌 Space Science & Physics Problem Solving', clubIds: ['stellar_space_tech', 'infinitus'] },
{ label: '🎙️ Public Speaking, Interviewing & Voice Recording', clubIds: ['podx', 'mediastic'] },
{ label: '🤝 Social Work, Environmental Protection & Leadership', clubIds: ['nss', 'unnat_bharat'] },
{ label: '🕺 Stage Performance, Singing, Dance & Rap', clubIds: ['artist_league'] },
{ label: '📐 Analytical Thinking, Logic Puzzles & Aptitude', clubIds: ['infinitus'] },
{ label: '📝 Essay Writing, Tamil/English Oratory & Literature', clubIds: ['vaarithi', 'fusion'] },
{ label: '🎥 Camera Operations, Video Editing & Digital Media', clubIds: ['mediastic', 'helios'] },
],
},
{
id: 'goal',
title: '3. What is your main goal for joining a club at RIT?',
options: [
{ label: '🤖 Build hackathon projects, chip designs & STEM innovations', clubIds: ['steam', 'wistem', 'techspark'] },
{ label: '🚀 Work on aerospace tech & scientific projects', clubIds: ['stellar_space_tech', 'infinitus'] },
{ label: '🌟 Share inspiring stories & host campus podcasts', clubIds: ['podx', 'mediastic'] },
{ label: '👥 Drive social change & serve rural communities', clubIds: ['nss', 'unnat_bharat'] },
{ label: '🏆 Perform live at cultural fests & stage shows', clubIds: ['artist_league', 'vaarithi'] },
{ label: '📖 Master new languages & explore world cultures', clubIds: ['nippon', 'fusion', 'vaarithi'] },
],
},
];
export default function Events() {
const [currentIdx, setCurrentIdx] = useState(0);
const [selectedCategory, setSelectedCategory] = useState('All');
const [selectedClub, setSelectedClub] = useState<Club | null>(null);
const categories = ['All', 'Technical', 'Cultural', 'Seminar', 'Workshop'];
// ─── Interactive Like / Favorite System State ─────────────────────────────
const [likedClubs, setLikedClubs] = useState<Set<string>>(() => {
try {
const saved = localStorage.getItem('rit_freshers_liked_clubs');
return saved ? new Set(JSON.parse(saved)) : new Set(['podx', 'stellar_space_tech']);
} catch {
return new Set(['podx', 'stellar_space_tech']);
}
});
const filteredEvents = EVENTS_DATA.filter((e) =>
selectedCategory === 'All' || e.category === selectedCategory.toLowerCase()
);
const [likesMap, setLikesMap] = useState<Record<string, number>>(() => {
const initialMap: Record<string, number> = {};
CLUBS_DATA.forEach((c, idx) => {
initialMap[c.id] = Math.round(c.members * 0.42) + (idx % 5) * 14 + 35;
});
return initialMap;
});
const prev = () => setCurrentIdx((i) => (i === 0 ? EVENTS_DATA.length - 1 : i - 1));
const next = () => setCurrentIdx((i) => (i === EVENTS_DATA.length - 1 ? 0 : i + 1));
const toggleLike = (clubId: string) => {
setLikedClubs((prev) => {
const next = new Set(prev);
const isCurrentlyLiked = next.has(clubId);
if (isCurrentlyLiked) {
next.delete(clubId);
setLikesMap((l) => ({ ...l, [clubId]: Math.max(0, (l[clubId] || 1) - 1) }));
} else {
next.add(clubId);
setLikesMap((l) => ({ ...l, [clubId]: (l[clubId] || 0) + 1 }));
}
try {
localStorage.setItem('rit_freshers_liked_clubs', JSON.stringify(Array.from(next)));
} catch {}
return next;
});
};
const featured = EVENTS_DATA[currentIdx];
const featCfg = CATEGORY_COLORS[featured.category] || CATEGORY_COLORS.seminar;
// ─── Club Matcher Quiz State (Feature 1) ─────────────────────────────────
const [isQuizOpen, setIsQuizOpen] = useState(false);
const [quizStep, setQuizStep] = useState(0);
const [selectedAnswers, setSelectedAnswers] = useState<number[]>([]);
const [quizResults, setQuizResults] = useState<{ club: Club; score: number }[] | null>(null);
const handleSelectOption = (optionIdx: number) => {
const updated = [...selectedAnswers];
updated[quizStep] = optionIdx;
setSelectedAnswers(updated);
if (quizStep < QUIZ_QUESTIONS.length - 1) {
setQuizStep(quizStep + 1);
} else {
calculateQuizResults(updated);
}
};
const calculateQuizResults = (answers: number[]) => {
const scoreMap: Record<string, number> = {};
CLUBS_DATA.forEach((c) => (scoreMap[c.id] = 0));
answers.forEach((ansIdx, qIdx) => {
const option = QUIZ_QUESTIONS[qIdx].options[ansIdx];
option.clubIds.forEach((clubId, idx) => {
scoreMap[clubId] = (scoreMap[clubId] || 0) + (3 - idx);
});
});
const ranked = CLUBS_DATA.map((club) => ({
club,
score: scoreMap[club.id] || 0,
})).sort((a, b) => b.score - a.score);
setQuizResults(ranked.slice(0, 3));
};
const resetQuiz = () => {
setQuizStep(0);
setSelectedAnswers([]);
setQuizResults(null);
};
return (
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
@@ -43,221 +193,537 @@ export default function Events() {
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="container-custom">
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
Clubs &{' '}
Student{' '}
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
Events
Clubs
</span>
</h1>
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
Discover events, join clubs, and be part of the RIT community.
Explore official RIT student clubs & societies, leadership details, and community links.
</p>
</div>
</div>
<div className="container-custom py-10">
{/* Featured Event Carousel */}
<SectionTitle tag="Upcoming" title="Featured" highlight="Events" />
<div className="container-custom pt-10 pb-20 md:pb-28">
<AnimatedContainer className="relative mb-14">
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden" style={{ boxShadow: '0 8px 40px -8px rgba(0,0,0,0.1)' }}>
<AnimatePresence mode="wait">
<motion.div
key={featured.id}
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -30 }}
transition={{ duration: 0.3 }}
className="flex flex-col md:flex-row"
>
{/* Image placeholder */}
{/* ─── Feature 1: "Find My Ideal Club" Banner ────────────────────────── */}
<AnimatedContainer className="mb-14">
<div
className="w-full md:w-2/5 h-56 md:h-auto flex items-center justify-center"
style={{ background: 'linear-gradient(135deg, #1E293B, #334155)' }}
className="rounded-3xl p-6 md:p-8 text-white relative overflow-hidden border border-orange-400/30 flex flex-col md:flex-row items-center justify-between gap-6"
style={{ background: 'linear-gradient(135deg, #1E293B, #0F172A)' }}
>
<div className="text-center">
<div className="text-5xl mb-2">
{featured.category === 'technical' ? '💻' : featured.category === 'cultural' ? '🎭' : featured.category === 'seminar' ? '🎤' : '📅'}
</div>
<div className="text-white/60 text-sm">{featured.venue}</div>
</div>
</div>
{/* Content */}
<div className="flex-1 p-8">
<div className="flex items-center gap-2 mb-3">
<span
className="px-3 py-1 rounded-full text-xs font-semibold capitalize"
style={{ backgroundColor: featCfg.bg, color: featCfg.text, border: `1px solid ${featCfg.border}`, fontFamily: 'Poppins, sans-serif' }}
>
{featured.category}
</span>
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-600 border border-emerald-200" style={{ fontFamily: 'Poppins, sans-serif' }}>
Upcoming
</span>
</div>
<h3 className="text-2xl font-bold text-[#1E293B] mb-3" style={{ fontFamily: 'Playfair Display, serif' }}>
{featured.title}
</h3>
<p className="text-[#475569] text-sm leading-relaxed mb-5" style={{ fontFamily: 'Inter, sans-serif' }}>
{featured.description}
</p>
<div className="grid grid-cols-2 gap-3 mb-6">
{[
{ icon: Calendar, label: new Date(featured.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' }) },
{ icon: Clock, label: featured.time },
{ icon: MapPin, label: featured.venue },
{ icon: Users, label: featured.organizer },
].map((item, i) => (
<div key={i} className="flex items-center gap-2 text-sm text-[#475569]">
<item.icon className="w-4 h-4 text-[#F97316] shrink-0" />
<span style={{ fontFamily: 'Inter, sans-serif' }}>{item.label}</span>
</div>
))}
</div>
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.03 }}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-white text-sm font-semibold"
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
>
Register Now
<ExternalLink className="w-4 h-4" />
</motion.button>
<motion.button
whileHover={{ scale: 1.03 }}
className="px-5 py-2.5 rounded-xl text-sm font-semibold border-2 border-[#E5E7EB] text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
More Details
</motion.button>
</div>
</div>
</motion.div>
</AnimatePresence>
</div>
{/* Carousel Controls */}
<div className="flex items-center justify-center gap-3 mt-4">
<button onClick={prev} className="w-9 h-9 rounded-xl border border-[#E5E7EB] bg-white flex items-center justify-center text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all">
<ChevronLeft className="w-4 h-4" />
</button>
{EVENTS_DATA.map((_, i) => (
<button
key={i}
onClick={() => setCurrentIdx(i)}
className="transition-all rounded-full"
style={{ width: i === currentIdx ? '24px' : '8px', height: '8px', backgroundColor: i === currentIdx ? '#F97316' : '#E5E7EB' }}
{/* Background Glow */}
<div
className="absolute top-0 right-0 w-80 h-80 rounded-full opacity-20 pointer-events-none"
style={{ background: 'radial-gradient(circle, #F97316, transparent)' }}
/>
))}
<button onClick={next} className="w-9 h-9 rounded-xl border border-[#E5E7EB] bg-white flex items-center justify-center text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all">
<ChevronRight className="w-4 h-4" />
</button>
<div className="relative z-10 max-w-xl">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-orange-500/20 text-orange-400 text-xs font-semibold mb-3 border border-orange-500/30">
<Target className="w-3.5 h-3.5" />
<span>Interactive Club Matcher</span>
</div>
<h2 className="text-2xl md:text-3xl font-bold text-white mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
Not sure which club to join?
</h2>
<p className="text-slate-300 text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
Take our 3-step AI-powered Club Matcher quiz to get instant recommendations tailored to your interests, skills, and goals!
</p>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
onClick={() => { resetQuiz(); setIsQuizOpen(true); }}
className="relative z-10 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm flex items-center gap-2.5 shadow-lg shrink-0 cursor-pointer"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
>
<Sparkles className="w-4 h-4" />
Find My Ideal Club
<ArrowRight className="w-4 h-4" />
</motion.button>
</div>
</AnimatedContainer>
{/* All Events Grid */}
<div className="mb-14">
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
<h2 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>All Events</h2>
<div className="flex gap-2 flex-wrap">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className="px-3.5 py-1.5 rounded-xl text-xs font-medium transition-all"
{/* Clubs Directory Section */}
<SectionTitle tag="Official Directory" title="Student" highlight="Clubs" subtitle="Explore official RIT clubs & societies. Click any club to view full details, leadership, and community social links." />
{(() => {
const fullGridCount = Math.floor(CLUBS_DATA.length / 3) * 3;
const mainGridClubs = CLUBS_DATA.slice(0, fullGridCount);
const remainingClubs = CLUBS_DATA.slice(fullGridCount);
const renderClubCard = (club: Club) => {
const IconComponent = (club.icon && CLUB_ICON_MAP[club.icon]) || Atom;
const categoryColor = CLUB_CATEGORY_COLORS[club.category] || '#F97316';
const isLiked = likedClubs.has(club.id);
const likesCount = likesMap[club.id] || 0;
return (
<motion.div
whileHover={{ y: -4 }}
onClick={() => setSelectedClub(club)}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 cursor-pointer hover:border-[#F97316] transition-all flex flex-col justify-between h-full group"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
>
<div>
{/* Top Row: Tech Icon / Club Logo & Interactive Heart Like Button */}
<div className="flex items-center justify-between mb-3">
{club.logoUrl ? (
<div className="w-12 h-12 rounded-full border border-slate-100 p-0.5 shadow-sm bg-white overflow-hidden shrink-0 transition-transform group-hover:scale-105">
<img src={club.logoUrl} alt={club.name} className="w-full h-full object-cover rounded-full" />
</div>
) : (
<div
className="w-11 h-11 rounded-full flex items-center justify-center text-white shadow-sm transition-transform group-hover:scale-105"
style={{
fontFamily: 'Poppins, sans-serif',
backgroundColor: selectedCategory === cat ? '#F97316' : '#F8FAFC',
color: selectedCategory === cat ? 'white' : '#475569',
border: `1px solid ${selectedCategory === cat ? '#F97316' : '#E5E7EB'}`,
background: `linear-gradient(135deg, ${categoryColor}, ${categoryColor}DD)`,
}}
>
{cat}
</button>
))}
<IconComponent className="w-5 h-5 text-white" />
</div>
)}
{/* Like Button & Members Count */}
<div className="flex items-center gap-2">
<motion.button
whileHover={{ scale: 1.08 }}
whileTap={{ scale: 0.85 }}
onClick={(e) => {
e.stopPropagation();
toggleLike(club.id);
}}
className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold transition-all cursor-pointer border ${
isLiked
? 'bg-rose-50 border-rose-200 text-rose-600 shadow-2xs'
: 'bg-slate-50 border-slate-200 text-slate-500 hover:border-rose-300 hover:text-rose-500'
}`}
>
<Heart className={`w-3.5 h-3.5 ${isLiked ? 'fill-rose-500 text-rose-500' : ''}`} />
<span>{likesCount}</span>
</motion.button>
<div className="flex items-center gap-1 text-xs text-[#94A3B8] bg-slate-50 px-2 py-1 rounded-full border border-slate-100">
<Users className="w-3.5 h-3.5 text-slate-400" />
<span>{club.members}</span>
</div>
</div>
</div>
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
{filteredEvents.map((event) => {
const cfg = CATEGORY_COLORS[event.category] || CATEGORY_COLORS.seminar;
return (
<StaggerItem key={event.id}>
<motion.div
whileHover={{ y: -4 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex gap-4"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
>
<div className="w-14 h-14 rounded-xl flex flex-col items-center justify-center shrink-0" style={{ backgroundColor: cfg.bg }}>
<span className="text-xs font-bold" style={{ color: cfg.text, fontFamily: 'Poppins, sans-serif' }}>
{new Date(event.date).toLocaleDateString('en-IN', { day: 'numeric' })}
</span>
<span className="text-[10px]" style={{ color: cfg.text }}>
{new Date(event.date).toLocaleDateString('en-IN', { month: 'short' })}
</span>
</div>
<div className="flex-1">
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold capitalize" style={{ backgroundColor: cfg.bg, color: cfg.text, fontFamily: 'Poppins, sans-serif' }}>
{event.category}
</span>
<h3 className="text-sm font-semibold text-[#1E293B] mt-1.5 mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>{event.title}</h3>
<div className="flex items-center gap-3 text-xs text-[#94A3B8]">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{event.time}</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" />{event.venue}</span>
</div>
</div>
</motion.div>
</StaggerItem>
);
})}
</StaggerContainer>
{/* Club Title & Description */}
<h3 className="font-semibold text-[#1E293B] mb-1 group-hover:text-[#F97316] transition-colors" style={{ fontFamily: 'Poppins, sans-serif' }}>
{club.name}
</h3>
<p className="text-xs text-[#64748B] mb-3 line-clamp-2" style={{ fontFamily: 'Inter, sans-serif' }}>{club.description}</p>
</div>
{/* Clubs Section */}
<SectionTitle tag="Join a Club" title="Student" highlight="Clubs" subtitle="Explore clubs and societies at RIT. Find your passion and connect with like-minded peers." />
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{CLUBS_DATA.map((club) => (
<StaggerItem key={club.id}>
<motion.div
whileHover={{ y: -4 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
>
<div className="flex items-start justify-between mb-3">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center text-white font-bold text-lg"
style={{ background: `linear-gradient(135deg, ${CLUB_CATEGORY_COLORS[club.category] || '#F97316'}, ${CLUB_CATEGORY_COLORS[club.category] || '#F97316'}CC)`, fontFamily: 'Poppins, sans-serif' }}
>
{club.name[0]}
</div>
<div className="flex items-center gap-1 text-xs text-[#94A3B8]">
<Users className="w-3.5 h-3.5" />
<span>{club.members} members</span>
</div>
</div>
<h3 className="font-semibold text-[#1E293B] mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>{club.name}</h3>
<p className="text-xs text-[#94A3B8] mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>{club.description}</p>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 text-xs" style={{ color: CLUB_CATEGORY_COLORS[club.category] || '#F97316' }}>
{/* Bottom Row: Category Tag & View Details Button */}
<div className="flex items-center justify-between mt-auto pt-2 border-t border-slate-100">
<span className="flex items-center gap-1 text-xs font-medium" style={{ color: categoryColor }}>
<Tag className="w-3 h-3" />
{club.category}
</span>
<motion.button
whileHover={{ scale: 1.04 }}
className="px-4 py-1.5 rounded-lg text-xs font-semibold text-white"
className="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white flex items-center gap-1 cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
>
Join Club
View Details
</motion.button>
</div>
</motion.div>
);
};
return (
<StaggerContainer className="pb-8 space-y-6 md:space-y-7">
{/* 3-Column Grid for Full Rows */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-7">
{mainGridClubs.map((club) => (
<StaggerItem key={club.id} className="w-full h-full">
{renderClubCard(club)}
</StaggerItem>
))}
</StaggerContainer>
</div>
{/* Centered Flex Row for Remaining Clubs (WiSTEM & STEAM) */}
{remainingClubs.length > 0 && (
<div className="flex flex-wrap justify-center gap-6 md:gap-7">
{remainingClubs.map((club) => (
<StaggerItem key={club.id} className="w-full sm:w-[calc(50%-0.875rem)] lg:w-[calc(33.333%-1.167rem)]">
{renderClubCard(club)}
</StaggerItem>
))}
</div>
)}
</StaggerContainer>
);
})()}
</div>
{/* ─── Club Matcher Quiz Modal (Feature 1) ─────────────────────────────── */}
<AnimatePresence>
{isQuizOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-3xl border border-[#E5E7EB] max-w-xl w-full p-6 md:p-8 relative shadow-2xl overflow-hidden"
>
{/* Close Button */}
<button
onClick={() => setIsQuizOpen(false)}
className="absolute top-5 right-5 w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-200 transition-all cursor-pointer"
>
<X className="w-5 h-5" />
</button>
{!quizResults ? (
<div>
{/* Quiz Progress */}
<div className="flex items-center justify-between text-xs text-slate-500 mb-2 font-medium">
<span>Question {quizStep + 1} of {QUIZ_QUESTIONS.length}</span>
<span>Step {quizStep + 1} / {QUIZ_QUESTIONS.length}</span>
</div>
<div className="w-full h-1.5 bg-slate-100 rounded-full mb-6 overflow-hidden">
<div
className="h-full bg-[#F97316] transition-all duration-300 rounded-full"
style={{ width: `${((quizStep + 1) / QUIZ_QUESTIONS.length) * 100}%` }}
/>
</div>
{/* Question Header */}
<div className="flex items-center gap-2 mb-2 text-[#F97316]">
<Compass className="w-5 h-5" />
<span className="text-xs font-bold uppercase tracking-wider">Club Matcher Quiz</span>
</div>
<h3 className="text-xl font-bold text-[#1E293B] mb-5" style={{ fontFamily: 'Playfair Display, serif' }}>
{QUIZ_QUESTIONS[quizStep].title}
</h3>
{/* Options List */}
<div className="space-y-3 mb-6">
{QUIZ_QUESTIONS[quizStep].options.map((opt, idx) => (
<button
key={idx}
onClick={() => handleSelectOption(idx)}
className="w-full text-left p-4 rounded-2xl border border-slate-200 hover:border-[#F97316] hover:bg-orange-50/50 transition-all flex items-center justify-between text-sm font-medium text-slate-700 cursor-pointer group"
>
<span>{opt.label}</span>
<ArrowRight className="w-4 h-4 text-slate-300 group-hover:text-[#F97316] group-hover:translate-x-1 transition-all" />
</button>
))}
</div>
{quizStep > 0 && (
<button
onClick={() => setQuizStep(quizStep - 1)}
className="text-xs font-semibold text-slate-500 hover:text-slate-800 flex items-center gap-1 cursor-pointer"
>
Previous Question
</button>
)}
</div>
) : (
/* Results Screen */
<div>
<div className="text-center mb-6">
<div className="w-14 h-14 rounded-2xl bg-orange-100 text-[#F97316] flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>
Your Ideal Club Matches!
</h3>
<p className="text-xs text-slate-500 mt-1">Based on your interests & goals, here are your top recommended RIT clubs:</p>
</div>
{/* Top 3 Matches */}
<div className="space-y-3 mb-6">
{quizResults.map((item, i) => {
const IconComponent = (item.club.icon && CLUB_ICON_MAP[item.club.icon]) || Atom;
const catColor = CLUB_CATEGORY_COLORS[item.club.category] || '#F97316';
const matchPercent = i === 0 ? '98%' : i === 1 ? '91%' : '84%';
return (
<div
key={item.club.id}
onClick={() => {
setIsQuizOpen(false);
setSelectedClub(item.club);
}}
className="p-4 rounded-2xl border border-slate-200 hover:border-[#F97316] bg-slate-50 hover:bg-white cursor-pointer transition-all flex items-center justify-between"
>
<div className="flex items-center gap-3">
{item.club.logoUrl ? (
<div className="w-10 h-10 rounded-full border border-slate-100 p-0.5 shadow-sm bg-white overflow-hidden shrink-0">
<img src={item.club.logoUrl} alt={item.club.name} className="w-full h-full object-cover rounded-full" />
</div>
) : (
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white shrink-0"
style={{ background: `linear-gradient(135deg, ${catColor}, ${catColor}DD)` }}
>
<IconComponent className="w-5 h-5 text-white" />
</div>
)}
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-sm text-[#1E293B]">{item.club.name}</span>
{i === 0 && <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-100 text-amber-700">Top Match 🏆</span>}
</div>
<span className="text-xs text-slate-500">{item.club.category} Club {item.club.members} members</span>
</div>
</div>
<span className="px-3 py-1 rounded-full text-xs font-bold bg-emerald-50 text-emerald-600 border border-emerald-200 shrink-0">
{matchPercent} Match
</span>
</div>
);
})}
</div>
<div className="flex gap-3">
<button
onClick={resetQuiz}
className="flex-1 py-3 rounded-xl border border-slate-300 text-slate-600 font-semibold text-xs flex items-center justify-center gap-1.5 hover:bg-slate-50 transition-all cursor-pointer"
>
<RotateCcw className="w-3.5 h-3.5" />
Retake Quiz
</button>
<button
onClick={() => setIsQuizOpen(false)}
className="flex-1 py-3 rounded-xl text-white font-semibold text-xs bg-[#F97316] hover:bg-[#EA580C] transition-all cursor-pointer"
>
Explore All Clubs
</button>
</div>
</div>
)}
</motion.div>
</div>
)}
</AnimatePresence>
{/* ─── Club Details Modal (Featuring Feature 5: Social Link Hub) ───────── */}
<AnimatePresence>
{selectedClub && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-3xl border border-[#E5E7EB] max-w-xl w-full max-h-[90vh] overflow-y-auto p-6 md:p-8 relative"
style={{ boxShadow: '0 20px 50px rgba(0,0,0,0.2)' }}
>
{/* Close Button */}
<button
onClick={() => setSelectedClub(null)}
className="absolute top-5 right-5 w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-200 transition-all cursor-pointer"
>
<X className="w-5 h-5" />
</button>
{/* Modal Header */}
{(() => {
const IconComponent = (selectedClub.icon && CLUB_ICON_MAP[selectedClub.icon]) || Atom;
const categoryColor = CLUB_CATEGORY_COLORS[selectedClub.category] || '#F97316';
const isLiked = likedClubs.has(selectedClub.id);
const likesCount = likesMap[selectedClub.id] || 0;
return (
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5 pb-4 border-b border-slate-100">
<div className="flex items-center gap-4">
{selectedClub.logoUrl ? (
<div className="w-16 h-16 rounded-full border-2 border-slate-100 p-1 shadow-md bg-white overflow-hidden shrink-0">
<img src={selectedClub.logoUrl} alt={selectedClub.name} className="w-full h-full object-cover rounded-full" />
</div>
) : (
<div
className="w-14 h-14 rounded-full flex items-center justify-center text-white font-bold text-2xl shadow-sm shrink-0"
style={{ background: `linear-gradient(135deg, ${categoryColor}, ${categoryColor}DD)` }}
>
<IconComponent className="w-7 h-7 text-white" />
</div>
)}
<div>
<span
className="px-2.5 py-0.5 rounded-full text-xs font-semibold"
style={{ backgroundColor: `${categoryColor}15`, color: categoryColor, fontFamily: 'Poppins, sans-serif' }}
>
{selectedClub.category}
</span>
<h2 className="text-2xl font-bold text-[#1E293B] mt-1" style={{ fontFamily: 'Playfair Display, serif' }}>
{selectedClub.name}
</h2>
</div>
</div>
{/* Interactive Like Button in Modal */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.92 }}
onClick={() => toggleLike(selectedClub.id)}
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 transition-all cursor-pointer shadow-xs border shrink-0 ${
isLiked
? 'bg-rose-500 text-white border-rose-600 shadow-rose-200'
: 'bg-rose-50 text-rose-600 border-rose-200 hover:bg-rose-100'
}`}
>
<Heart className={`w-4 h-4 ${isLiked ? 'fill-white' : 'text-rose-500'}`} />
<span>{isLiked ? 'Liked' : 'Like Club'} ({likesCount})</span>
</motion.button>
</div>
);
})()}
{/* Club Detailed Description */}
<div className="mb-5 bg-gradient-to-r from-orange-50/70 via-amber-50/40 to-slate-50 border border-orange-200/80 rounded-2xl p-4.5">
<div className="flex items-center gap-2 text-xs font-bold text-[#F97316] uppercase tracking-wider mb-2">
<Info className="w-4 h-4 text-[#F97316]" />
<span>About the Club</span>
</div>
<p className="text-slate-700 text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
{selectedClub.details || selectedClub.description}
</p>
</div>
{/* Leadership & Contact Information - Icon Based Color Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5 mb-6">
{/* President Card - Indigo Theme */}
<div className="bg-indigo-50/80 border border-indigo-100 hover:border-indigo-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 text-white flex items-center justify-center shadow-xs shadow-indigo-200 shrink-0">
<UserCheck className="w-5 h-5 text-white" />
</div>
<div>
<span className="text-[11px] text-indigo-500 font-semibold block">President / Student Lead</span>
<span className="text-sm font-bold text-indigo-950">{selectedClub.presidentName || 'Student President'}</span>
</div>
</div>
{/* Year Card - Purple Theme */}
<div className="bg-purple-50/80 border border-purple-100 hover:border-purple-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white flex items-center justify-center shadow-xs shadow-purple-200 shrink-0">
<GraduationCap className="w-5 h-5 text-white" />
</div>
<div>
<span className="text-[11px] text-purple-500 font-semibold block">Year & Department</span>
<span className="text-sm font-bold text-purple-950">{selectedClub.year || 'Senior Year'}</span>
</div>
</div>
{/* Email Card - Emerald Theme */}
<div className="bg-emerald-50/80 border border-emerald-100 hover:border-emerald-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 text-white flex items-center justify-center shadow-xs shadow-emerald-200 shrink-0">
<Mail className="w-5 h-5 text-white" />
</div>
<div className="min-w-0 flex-1">
<span className="text-[11px] text-emerald-500 font-semibold block">Contact Email</span>
<a href={`mailto:${selectedClub.contactEmail}`} className="text-sm font-bold text-emerald-700 hover:text-emerald-800 hover:underline truncate block">
{selectedClub.contactEmail || 'club@ritchennai.edu.in'}
</a>
</div>
</div>
{/* Phone Card - Sky Theme */}
<div className="bg-sky-50/80 border border-sky-100 hover:border-sky-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-sky-500 to-sky-600 text-white flex items-center justify-center shadow-xs shadow-sky-200 shrink-0">
<Phone className="w-5 h-5 text-white" />
</div>
<div>
<span className="text-[11px] text-sky-500 font-semibold block">Contact Phone</span>
<span className="text-sm font-bold text-sky-950">{selectedClub.contactPhone || '+91 98765 43210'}</span>
</div>
</div>
</div>
{/* ─── Feature 5: Social & Community Link Hub (Dummy Links) ─────── */}
<div className="mb-6 bg-orange-50/60 border border-orange-200/80 rounded-2xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2 text-xs font-bold text-[#F97316] uppercase tracking-wider">
<ExternalLink className="w-3.5 h-3.5" />
<span>Community & Social Links</span>
</div>
<span className="text-[10px] text-emerald-600 font-medium"> Verified RIT Handle</span>
</div>
<div className="grid grid-cols-2 gap-2.5">
{selectedClub.socialLinks?.instagram && (
<a
href={selectedClub.socialLinks.instagram}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-pink-500 hover:text-pink-600 transition-all text-xs font-medium text-slate-700"
>
<InstagramIcon className="w-4 h-4 text-pink-500 shrink-0" />
<span className="truncate">Instagram</span>
</a>
)}
{selectedClub.socialLinks?.website && (
<a
href={selectedClub.socialLinks.website}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-orange-500 hover:text-orange-600 transition-all text-xs font-medium text-slate-700"
>
<Globe className="w-4 h-4 text-orange-500 shrink-0" />
<span className="truncate">Linktree Hub</span>
</a>
)}
{selectedClub.socialLinks?.linkedin && (
<a
href={selectedClub.socialLinks.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-blue-600 hover:text-blue-600 transition-all text-xs font-medium text-slate-700"
>
<LinkedinIcon className="w-4 h-4 text-blue-600 shrink-0" />
<span className="truncate">LinkedIn</span>
</a>
)}
{selectedClub.socialLinks?.whatsapp && (
<a
href={selectedClub.socialLinks.whatsapp}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-emerald-500 hover:text-emerald-600 transition-all text-xs font-medium text-slate-700"
>
<MessageCircle className="w-4 h-4 text-emerald-500 shrink-0" />
<span className="truncate">WhatsApp Group</span>
</a>
)}
{selectedClub.socialLinks?.youtube && (
<a
href={selectedClub.socialLinks.youtube}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-red-600 hover:text-red-600 transition-all text-xs font-medium text-slate-700"
>
<YoutubeIcon className="w-4 h-4 text-red-600 shrink-0" />
<span className="truncate">YouTube Channel</span>
</a>
)}
</div>
</div>
{/* Close Button */}
<div>
<button
onClick={() => setSelectedClub(null)}
className="w-full py-3 rounded-xl border border-slate-300 text-slate-700 font-semibold text-sm hover:bg-slate-100 transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
Close
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -1,196 +1,57 @@
import { motion } from 'framer-motion';
import { motion, useScroll, useTransform } from 'framer-motion';
import { Link } from 'react-router-dom';
import {
ArrowRight, Sparkles, BookOpen, Bot, GraduationCap,
CheckCircle, Zap, Shield, Brain
CheckCircle, Zap, Shield, Brain, Trophy, Award, Cpu, Users
} from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import StatsCard from '@/components/StatsCard/StatsCard';
import FeatureCard from '@/components/FeatureCard/FeatureCard';
import FloatingCard from '@/components/FloatingCard/FloatingCard';
import ChatPreview from '@/components/ChatPreview/ChatPreview';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { STATS, FEATURES, CAMPUS_LOCATIONS } from '@/constants';
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
import * as LucideIcons from 'lucide-react';
export default function Home() {
const { scrollY } = useScroll();
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
const heroY = useTransform(scrollY, [0, 450], [0, -80]);
return (
<div>
<div className="relative">
{/* Sticky Background Video */}
<div className="fixed inset-0 w-full h-full z-0 pointer-events-none">
<video
autoPlay
muted
loop
playsInline
preload="auto"
className="w-full h-full object-cover"
style={{
filter: 'contrast(1.12) brightness(0.88) saturate(1.08)',
transform: 'translate3d(0, 0, 0)',
willChange: 'transform',
imageRendering: 'high-quality'
}}
>
<source src="/video/RIT Video.mp4" type="video/mp4" />
</video>
{/* Dark Overlay (40-50%) */}
<div
className="absolute inset-0"
style={{ background: 'linear-gradient(to bottom, rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.65))' }}
/>
</div>
{/* ─── Hero Section ─────────────────────────────────────────────────────── */}
<section className="relative overflow-hidden pt-12 pb-24" style={{ backgroundColor: '#FAFAFA' }}>
{/* Background Decoration */}
<div
className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full opacity-[0.04] -translate-y-1/2 translate-x-1/3 pointer-events-none"
style={{ background: 'radial-gradient(circle, #F97316, transparent)' }}
/>
<div
className="absolute bottom-0 left-0 w-[350px] h-[350px] rounded-full opacity-[0.03] translate-y-1/2 -translate-x-1/3 pointer-events-none"
style={{ background: 'radial-gradient(circle, #F97316, transparent)' }}
/>
<div className="container-custom">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
{/* Left Visual Section (RIT Events Hub Style) */}
<div className="lg:col-span-6 relative flex justify-center">
<section className="relative overflow-hidden min-h-screen flex flex-col justify-center pt-24 pb-32 z-10 bg-transparent">
{/* Center Content */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.7 }}
className="relative w-full max-w-lg"
className="container-custom relative z-20 flex flex-col items-center justify-center text-center px-4"
style={{ opacity: heroOpacity, y: heroY }}
>
{/* Main campus image/graphic card */}
<div
className="rounded-[24px] overflow-hidden bg-white border border-[#E5E7EB] relative aspect-[4/3] w-full"
style={{ boxShadow: '0 24px 60px -12px rgba(0,0,0,0.15)' }}
>
{/* Inline Premium Vector SVG Illustration of RIT Campus Building & Canopy */}
<svg viewBox="0 0 800 600" className="w-full h-full select-none" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="skyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#1E3A8A" />
<stop offset="40%" stopColor="#3B82F6" />
<stop offset="100%" stopColor="#93C5FD" />
</linearGradient>
<linearGradient id="grassGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#059669" />
<stop offset="100%" stopColor="#065F46" />
</linearGradient>
<linearGradient id="pathGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#F1F5F9" />
<stop offset="100%" stopColor="#CBD5E1" />
</linearGradient>
<linearGradient id="buildGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#E2E8F0" />
<stop offset="100%" stopColor="#F8FAFC" />
</linearGradient>
<linearGradient id="canopyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#F97316" />
<stop offset="100%" stopColor="#EA580C" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="15" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Sky */}
<rect width="800" height="600" fill="url(#skyGrad)" />
{/* Glowing Sun */}
<circle cx="700" cy="100" r="50" fill="#FEF08A" opacity="0.8" filter="url(#glow)" />
{/* Distant Hills / Tree Line */}
<path d="M0 450 Q150 420 300 440 T600 430 T800 450 L800 600 L0 600 Z" fill="#047857" opacity="0.7" />
<path d="M0 460 Q200 440 400 455 T800 460 L800 600 L0 600 Z" fill="url(#grassGrad)" />
{/* Academic Building Facade (Modern RIT Blocks) */}
<rect x="50" y="200" width="300" height="260" fill="url(#buildGrad)" rx="8" />
{/* Windows Grid */}
<g fill="#475569" opacity="0.3">
{/* Row 1 */}
<rect x="75" y="230" width="35" height="40" rx="3" />
<rect x="135" y="230" width="35" height="40" rx="3" />
<rect x="195" y="230" width="35" height="40" rx="3" />
<rect x="255" y="230" width="35" height="40" rx="3" />
{/* Row 2 */}
<rect x="75" y="300" width="35" height="40" rx="3" />
<rect x="135" y="300" width="35" height="40" rx="3" />
<rect x="195" y="300" width="35" height="40" rx="3" />
<rect x="255" y="300" width="35" height="40" rx="3" />
{/* Row 3 */}
<rect x="75" y="370" width="35" height="40" rx="3" />
<rect x="135" y="370" width="35" height="40" rx="3" />
<rect x="195" y="370" width="35" height="40" rx="3" />
<rect x="255" y="370" width="35" height="40" rx="3" />
</g>
{/* Main Entrance Pillars & Arch (RIT Architecture) */}
<rect x="380" y="150" width="380" height="310" fill="#F1F5F9" rx="16" opacity="0.95" />
<rect x="400" y="170" width="340" height="290" fill="#E2E8F0" rx="12" />
{/* Premium Canopy Truss Structure (RIT Event Canopy Representation) */}
<polygon points="360,150 780,120 780,180 360,210" fill="url(#canopyGrad)" opacity="0.9" />
<g stroke="white" strokeWidth="3" opacity="0.8">
<line x1="380" y1="155" x2="380" y2="200" />
<line x1="450" y1="150" x2="450" y2="195" />
<line x1="520" y1="145" x2="520" y2="190" />
<line x1="590" y1="140" x2="590" y2="185" />
<line x1="660" y1="135" x2="660" y2="180" />
<line x1="730" y1="130" x2="730" y2="175" />
{/* Cross trusses */}
<line x1="380" y1="155" x2="450" y2="195" />
<line x1="450" y1="150" x2="520" y2="190" />
<line x1="520" y1="145" x2="590" y2="185" />
<line x1="590" y1="140" x2="660" y2="180" />
<line x1="660" y1="135" x2="730" y2="175" />
</g>
{/* Columns supporting Canopy */}
<rect x="400" y="195" width="25" height="265" fill="#64748B" rx="4" />
<rect x="520" y="190" width="25" height="270" fill="#64748B" rx="4" />
<rect x="640" y="180" width="25" height="280" fill="#64748B" rx="4" />
<rect x="730" y="175" width="25" height="285" fill="#64748B" rx="4" />
{/* Campus Pathway leading to building */}
<polygon points="350,460 550,460 700,600 100,600" fill="url(#pathGrad)" />
<line x1="400" y1="460" x2="250" y2="600" stroke="#CBD5E1" strokeWidth="4" />
<line x1="500" y1="460" x2="550" y2="600" stroke="#CBD5E1" strokeWidth="4" />
{/* Trees & Foliage in Foreground */}
<g fill="#059669">
<circle cx="80" cy="480" r="45" />
<circle cx="120" cy="510" r="35" />
<circle cx="720" cy="500" r="50" />
<circle cx="760" cy="480" r="40" />
</g>
<g fill="#10B981">
<circle cx="75" cy="465" r="35" />
<circle cx="715" cy="485" r="40" />
</g>
</svg>
{/* Subtle dark gradient overlay to ensure text contrast */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-black/10 pointer-events-none" />
{/* Text on top of the image (RIT Events Hub Style) */}
<div className="absolute bottom-6 left-6 z-10 text-left pointer-events-none">
<h2 className="text-white font-extrabold text-2xl tracking-wide leading-tight" style={{ fontFamily: 'Playfair Display, serif' }}>
Rajalakshmi Institute
</h2>
<p className="text-orange-400 font-bold text-sm mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>
of Technology, Chennai
</p>
<div className="inline-flex items-center gap-1.5 mt-3.5 px-3 py-1 rounded-full bg-white/10 backdrop-blur-sm border border-white/20">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-[10px] text-slate-300 font-bold uppercase tracking-wider">Est. 2008 NBA & NAAC A+</span>
</div>
</div>
</div>
{/* Overlapping Dark Card (Innovation Card - Exact Match with Reference Image) */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="absolute -bottom-6 -right-4 md:-right-8 bg-[#1E293B] text-white p-6 rounded-2xl border border-white/10 max-w-[240px] z-20"
style={{ boxShadow: '0 12px 40px rgba(0,0,0,0.25)' }}
>
<span className="text-[#F97316] text-[11px] font-bold tracking-wider uppercase mb-1.5 block" style={{ fontFamily: 'Poppins, sans-serif' }}>
INNOVATION
</span>
<p className="text-[12px] text-slate-300 leading-relaxed font-semibold" style={{ fontFamily: 'Inter, sans-serif' }}>
Where engineering meets guidance. Fueling the academic journey of our freshers.
</p>
</motion.div>
</motion.div>
</div>
{/* Right Text Content */}
<div className="lg:col-span-6 flex flex-col justify-center">
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
@@ -203,39 +64,47 @@ export default function Home() {
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-4xl md:text-5xl xl:text-6xl font-bold leading-[1.1] mb-6"
style={{ fontFamily: 'Playfair Display, serif', color: '#1E293B' }}
className="text-white text-3xl md:text-4xl font-light mb-2"
style={{ fontFamily: 'Playfair Display, serif', color: '#FFFFFF', textShadow: '0 2px 10px rgba(0, 0, 0, 0.5)' }}
>
Welcome to <br />
<span className="font-serif italic text-[#F97316] font-medium block mt-1">
RIT Freshers Hub
</span>
Welcome to
</motion.h1>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }}
className="text-5xl md:text-7xl font-extrabold leading-tight mb-6 bg-gradient-to-r from-[#F97316] via-[#FB923C] to-[#F97316] bg-clip-text text-transparent"
style={{ fontFamily: 'Playfair Display, serif', color: 'transparent', WebkitTextFillColor: 'transparent', filter: 'drop-shadow(0 4px 12px rgba(249, 115, 22, 0.45))' }}
>
RIT Freshers Hub
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-base text-[#475569] leading-relaxed mb-8"
transition={{ duration: 0.7, delay: 0.3 }}
className="max-w-2xl text-slate-200 text-sm md:text-base leading-relaxed mb-8"
style={{ fontFamily: 'Inter, sans-serif' }}
>
The RIT Freshers Hub is your centralized gateway to campus life, designed to simplify how you discover and participate in academic and extracurricular activities. With seamless access to notes, PYQs, AI assistant, and bus routes, we ensure you have everything needed to succeed.
The RIT Freshers Hub is your centralized gateway to campus life, academic resources, AI assistance, clubs, events, bus routes, notes, and student services.
</motion.p>
{/* Buttons */}
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-wrap gap-4 mb-8"
transition={{ duration: 0.7, delay: 0.4 }}
className="flex flex-wrap justify-center gap-4 mb-8"
>
<Link to="/notes">
<a href="#built-for-freshers">
<motion.button
whileHover={{ scale: 1.03, boxShadow: '0 8px 25px rgba(249,115,22,0.3)' }}
whileTap={{ scale: 0.97 }}
className="flex items-center gap-2 px-6 py-3 rounded-xl text-white font-semibold text-sm cursor-pointer"
whileHover={{ scale: 1.05, boxShadow: '0 8px 25px rgba(249,115,22,0.4)' }}
whileTap={{ scale: 0.95 }}
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-semibold text-sm cursor-pointer"
style={{
fontFamily: 'Poppins, sans-serif',
background: 'linear-gradient(135deg, #F97316, #FB923C)',
@@ -245,13 +114,13 @@ export default function Home() {
Explore Hub
<ArrowRight className="w-3.5 h-3.5" />
</motion.button>
</Link>
</a>
<Link to="/ai-assistant">
<motion.button
whileHover={{ scale: 1.03, borderColor: '#F97316', color: '#F97316', backgroundColor: '#FFF7ED' }}
whileTap={{ scale: 0.97 }}
className="flex items-center gap-2 px-6 py-3 rounded-xl font-semibold text-sm border-2 border-[#E5E7EB] text-[#1E293B] bg-white transition-all cursor-pointer"
whileHover={{ scale: 1.05, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.15)' }}
whileTap={{ scale: 0.95 }}
className="flex items-center gap-2 px-8 py-3.5 rounded-xl font-semibold text-sm border-2 border-white/20 text-white bg-white/10 backdrop-blur-md transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<Bot className="w-4 h-4" />
@@ -260,44 +129,87 @@ export default function Home() {
</Link>
</motion.div>
{/* Badges positioned cleanly below buttons with more breathing room */}
{/* Badges */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="flex flex-wrap gap-6 text-[#94A3B8] border-t border-[#E5E7EB] pt-6"
transition={{ duration: 0.8, delay: 0.5 }}
className="flex flex-wrap justify-center gap-6 text-slate-300 border-t border-white/10 pt-6 max-w-lg w-full"
>
{[
{ icon: CheckCircle, text: 'Official Portal' },
{ icon: Zap, text: 'Instant AI Support' },
{ icon: Shield, text: 'Student Verified' },
].map((badge, i) => (
<div key={i} className="flex items-center gap-2 text-xs font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>
<badge.icon className="w-4 h-4 text-[#F97316]/70" />
<div key={i} className="flex items-center gap-1.5 text-xs font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>
<badge.icon className="w-4 h-4 text-[#F97316]" />
<span>{badge.text}</span>
</div>
))}
</motion.div>
</div>
</div>
</div>
</motion.div>
</section>
{/* ─── Stats Section ────────────────────────────────────────────────────── */}
<section className="py-16 bg-white">
<div className="container-custom">
<StaggerContainer className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
{STATS.map((stat, i) => (
<StaggerItem key={i}>
<StatsCard stat={stat} delay={i * 0.05} />
</StaggerItem>
{/* Floating Glassmorphic Stats Section */}
<section className="relative z-30 -mt-16 px-4">
<div className="container-custom max-w-5xl">
<style>{`
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
`}</style>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
className="animate-float rounded-[24px] border border-white/10 p-6 md:p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.4)]"
style={{
background: 'linear-gradient(135deg, rgba(11, 19, 43, 0.9) 0%, rgba(15, 23, 42, 0.8) 100%)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
}}
>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-6 text-center">
{[
{ value: '20+', label: 'SPORTS', icon: Trophy },
{ value: '100+', label: 'FACULTY', icon: GraduationCap },
{ value: '18', label: 'CLUBS', icon: Award },
{ value: '24/7', label: 'AI ASSISTANT', icon: Cpu },
{ value: '5000+', label: 'STUDENTS', icon: Users },
].map((stat, i) => (
<div key={i} className="flex flex-col items-center justify-center p-4">
{/* Icon */}
<div className="w-10 h-10 rounded-xl bg-[#F97316]/10 flex items-center justify-center mb-3">
<stat.icon className="w-5 h-5 text-[#F97316]" />
</div>
{/* Value */}
<div
className="text-2xl md:text-3xl font-extrabold text-white mb-1"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
{stat.value}
</div>
{/* Label */}
<div
className="text-[10px] md:text-xs font-semibold tracking-wider text-slate-300 uppercase whitespace-nowrap"
style={{ fontFamily: 'Inter, sans-serif' }}
>
{stat.label}
</div>
</div>
))}
</StaggerContainer>
</div>
</motion.div>
</div>
</section>
{/* ─── Features Section ─────────────────────────────────────────────────── */}
<section className="section-padding" style={{ backgroundColor: '#FAFAFA' }}>
<section id="built-for-freshers" className="section-padding relative z-20" style={{ backgroundColor: '#FAFAFA' }}>
<div className="container-custom">
<SectionTitle
tag="Everything You Need"
@@ -317,7 +229,7 @@ export default function Home() {
</section>
{/* ─── AI Section ───────────────────────────────────────────────────────── */}
<section className="section-padding bg-white">
<section className="section-padding bg-white relative z-20">
<div className="container-custom">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Left Text */}
@@ -346,11 +258,11 @@ export default function Home() {
{ icon: 'Map', label: 'Campus Context', desc: 'RIT-specific answers' },
{ icon: 'Zap', label: 'Instant Answers', desc: 'No wait time' },
].map((item, i) => {
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[item.icon];
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>>)[item.icon];
return (
<div key={i} className="flex items-start gap-3 p-4 rounded-xl border border-[#E5E7EB] bg-[#FAFAFA]">
<div className="w-9 h-9 rounded-lg flex items-center justify-center bg-[#FFF7ED] shrink-0">
{Icon && <Icon className="w-4.5 h-4.5" style={{ color: '#F97316' } as React.CSSProperties} />}
{Icon && <Icon className="w-4.5 h-4.5" style={{ color: '#F97316' }} />}
</div>
<div>
<div className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>{item.label}</div>
@@ -388,7 +300,7 @@ export default function Home() {
</section>
{/* ─── Campus Quick Nav ─────────────────────────────────────────────────── */}
<section className="section-padding" style={{ backgroundColor: '#FAFAFA' }}>
<section className="section-padding relative z-20" style={{ backgroundColor: '#FAFAFA' }}>
<div className="container-custom">
<SectionTitle
tag="Navigate Campus"
@@ -450,7 +362,7 @@ export default function Home() {
{/* Quick nav buttons */}
<div className="p-5 grid grid-cols-4 sm:grid-cols-8 gap-3">
{CAMPUS_LOCATIONS.map((loc) => {
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
return (
<Link to="/campus" key={loc.id}>
<motion.div
@@ -474,7 +386,7 @@ export default function Home() {
</section>
{/* ─── Community Teaser ─────────────────────────────────────────────────── */}
<section className="section-padding bg-white">
<section className="section-padding bg-white relative z-20">
<div className="container-custom">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Q&A teaser */}

View File

@@ -1,11 +1,37 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Search, Download, Filter, BookOpen, FileText, ScrollText, BookMarked, ChevronRight } from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
const SEMESTERS = ['All', '1st Sem', '2nd Sem', '3rd Sem', '4th Sem', '5th Sem', '6th Sem', '7th Sem', '8th Sem'];
const SEMESTERS = ['All', '1st Sem', '2nd Sem'];
const SUBJECTS_BY_SEM: Record<number, string[]> = {
1: [
"Communicative English",
"Matrices and Calculus",
"Physics for Information Science",
"Problem Solving and C Programming",
"Basic Electrical and Electronics Engineering",
"Heritage of Tamils",
"Physics Laboratory",
"Problem Solving and C Programming Laboratory",
"Engineering Practices Laboratory"
],
2: [
"Professional English",
"Engineering Chemistry",
"Statistics and Numerical Methods",
"Python for Data Science",
"Tamils and Technology",
"Engineering Graphics",
"Data Structures Design",
"Chemistry Laboratory",
"Python for Data Science Laboratory",
"Communication Laboratory"
]
};
const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.ComponentType<{ className?: string }> }> = {
notes: { bg: '#EFF6FF', text: '#3B82F6', icon: BookOpen },
@@ -14,30 +40,74 @@ const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.Compon
assignment: { bg: '#FDF2F8', text: '#EC4899', icon: BookMarked },
};
const NOTES_DATA = [
{ id: '1', title: 'Engineering Mathematics Unit 1-5', subject: 'Mathematics', department: 'Computer Science & Engineering', semester: 1, type: 'notes', downloads: 348, fileSize: '4.2 MB', uploadedAt: '2025-06-01' },
{ id: '2', title: 'Physics PYQ 2020-2024', subject: 'Physics', department: 'Computer Science & Engineering', semester: 1, type: 'pyq', downloads: 512, fileSize: '8.1 MB', uploadedAt: '2025-06-10' },
{ id: '3', title: 'C Programming Complete Notes', subject: 'Programming', department: 'Computer Science & Engineering', semester: 1, type: 'notes', downloads: 734, fileSize: '6.3 MB', uploadedAt: '2025-06-15' },
{ id: '4', title: 'Data Structures PYQ 2019-2024', subject: 'Data Structures', department: 'Computer Science & Engineering', semester: 3, type: 'pyq', downloads: 621, fileSize: '10.2 MB', uploadedAt: '2025-07-01' },
{ id: '5', title: 'Circuit Theory Full Notes', subject: 'Circuit Theory', department: 'Electronics & Communication', semester: 2, type: 'notes', downloads: 289, fileSize: '5.7 MB', uploadedAt: '2025-06-20' },
{ id: '6', title: 'Anna University Syllabus 2021', subject: 'Syllabus', department: 'All Departments', semester: 1, type: 'syllabus', downloads: 890, fileSize: '2.1 MB', uploadedAt: '2025-05-01' },
{ id: '7', title: 'Thermodynamics Notes', subject: 'Thermodynamics', department: 'Mechanical Engineering', semester: 3, type: 'notes', downloads: 201, fileSize: '3.9 MB', uploadedAt: '2025-06-25' },
{ id: '8', title: 'Digital Electronics PYQ', subject: 'Digital Electronics', department: 'Electronics & Communication', semester: 4, type: 'pyq', downloads: 445, fileSize: '7.3 MB', uploadedAt: '2025-07-05' },
];
interface NoteItem {
id: string;
title: string;
subject: string;
department: string;
semester: number;
type: string;
downloads: number;
fileSize: string;
uploadedAt: string;
downloadUrl: string;
}
export default function Notes() {
const [notes, setNotes] = useState<NoteItem[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [selectedSem, setSelectedSem] = useState('All');
const [selectedDept, setSelectedDept] = useState('All Departments');
const [selectedType, setSelectedType] = useState('all');
const [selectedSubject, setSelectedSubject] = useState('All Subjects');
const filtered = NOTES_DATA.filter((note) => {
useEffect(() => {
fetch('http://localhost:8080/api/notes')
.then((res) => res.json())
.then((data) => {
const mapped = data.map((item: any) => ({
id: item.id.toString(),
title: item.title,
subject: item.subject,
department: item.department,
semester: item.semester,
type: item.fileType,
downloads: item.downloadsCount,
fileSize: item.fileSize,
uploadedAt: item.uploadedAt ? item.uploadedAt.split('T')[0] : '',
downloadUrl: item.downloadUrl,
}));
setNotes(mapped);
})
.catch((err) => console.error('Error fetching notes:', err));
}, []);
const handleDownload = async (note: NoteItem) => {
// Open in new tab
window.open(note.downloadUrl, '_blank');
// Call backend to increment download count
try {
await fetch(`http://localhost:8080/api/notes/${note.id}/download`, {
method: 'POST',
});
// Increment locally to update UI immediately
setNotes((prevNotes) =>
prevNotes.map((n) => (n.id === note.id ? { ...n, downloads: n.downloads + 1 } : n))
);
} catch (err) {
console.error('Error incrementing download count:', err);
}
};
const filtered = notes.filter((note) => {
const matchSearch = note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.subject.toLowerCase().includes(searchQuery.toLowerCase());
const matchSem = selectedSem === 'All' || note.semester === parseInt(selectedSem);
const matchDept = selectedDept === 'All Departments' || note.department === selectedDept || note.department === 'All Departments';
const matchType = selectedType === 'all' || note.type === selectedType;
return matchSearch && matchSem && matchDept && matchType;
const matchSubject = selectedSubject === 'All Subjects' || note.subject === selectedSubject;
return matchSearch && matchSem && matchDept && matchType && matchSubject;
});
return (
@@ -90,7 +160,7 @@ export default function Notes() {
{/* Type Filter */}
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-[#94A3B8]" />
{['all', 'notes', 'pyq', 'syllabus'].map((type) => (
{['all', 'notes', 'pyq', 'syllabus', 'assignment'].map((type) => (
<button
key={type}
onClick={() => setSelectedType(type)}
@@ -109,10 +179,13 @@ export default function Notes() {
{/* Semester Filter */}
<div className="flex gap-2 flex-wrap">
{SEMESTERS.slice(0, 5).map((sem) => (
{SEMESTERS.map((sem) => (
<button
key={sem}
onClick={() => setSelectedSem(sem)}
onClick={() => {
setSelectedSem(sem);
setSelectedSubject('All Subjects');
}}
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
style={{
fontFamily: 'Poppins, sans-serif',
@@ -125,11 +198,58 @@ export default function Notes() {
</button>
))}
</div>
{/* Subject Dropdown */}
{selectedSem !== 'All' && (
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-[#64748B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Subject:</span>
<select
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-[#F8FAFC] border border-[#E5E7EB] text-[#475569] focus:outline-none focus:border-[#F97316] transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<option value="All Subjects">All Subjects</option>
{SUBJECTS_BY_SEM[parseInt(selectedSem)].map((sub) => (
<option key={sub} value={sub}>
{sub}
</option>
))}
</select>
</div>
)}
</div>
</motion.div>
{/* Stats Summary Banner */}
<div className="grid grid-cols-2 gap-4 bg-white border border-[#E5E7EB] rounded-2xl p-5 mb-8" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
{/* Stat 1: Notes */}
<div className="flex items-center justify-center gap-4">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-[#FFF7ED] shrink-0">
<FileText className="w-6 h-6 text-[#F97316]" />
</div>
<div className="text-left">
<div className="text-xl md:text-2xl font-bold text-[#1E293B]">{notes.length}+</div>
<div className="text-xs text-[#64748B] font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>Notes & Materials</div>
</div>
</div>
{/* Stat 2: Subjects */}
<div className="flex items-center justify-center gap-4 border-l border-[#E5E7EB]">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-[#EFF6FF] shrink-0">
<BookOpen className="w-6 h-6 text-[#3B82F6]" />
</div>
<div className="text-left pl-4">
<div className="text-xl md:text-2xl font-bold text-[#1E293B]">
{Array.from(new Set(notes.map(n => n.subject))).length}
</div>
<div className="text-xs text-[#64748B] font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>Subjects</div>
</div>
</div>
</div>
{/* Results */}
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-12">
<StaggerContainer key={filtered.length} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-12">
{filtered.map((note) => {
const typeConfig = TYPE_COLORS[note.type];
const TypeIcon = typeConfig.icon;
@@ -137,35 +257,64 @@ export default function Notes() {
<StaggerItem key={note.id}>
<motion.div
whileHover={{ y: -4 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex flex-col gap-3"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex flex-col justify-between gap-4"
style={{
borderLeft: `4px solid ${typeConfig.text}`,
boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)'
}}
>
<div className="flex items-start justify-between gap-2">
<div className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
<TypeIcon className="w-5 h-5" style={{ color: typeConfig.text } as React.CSSProperties} />
<div className="flex gap-4">
{/* Visual PDF Preview Icon */}
<div className="w-14 h-20 border border-slate-200 rounded-xl p-2 flex flex-col justify-between bg-[#F8FAFC] shrink-0 shadow-sm">
<div className="bg-[#EF4444] text-[8px] font-extrabold text-white px-1 py-0.5 rounded w-fit uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
PDF
</div>
<div className="flex flex-col gap-1.5">
<div className="w-full h-1 bg-slate-200 rounded" />
<div className="w-5/6 h-1 bg-slate-200 rounded" />
<div className="w-2/3 h-1 bg-slate-200 rounded" />
</div>
</div>
{/* Text Details */}
<div className="flex flex-col justify-between flex-1 min-w-0">
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="w-6 h-6 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
<TypeIcon className="w-3.5 h-3.5" style={{ color: typeConfig.text }} />
</div>
<span
className="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
className="px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
style={{ backgroundColor: typeConfig.bg, color: typeConfig.text, fontFamily: 'Poppins, sans-serif' }}
>
{note.type}
</span>
</div>
<div>
<h3 className="text-sm font-semibold text-[#1E293B] leading-snug mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
<h3 className="text-sm font-bold text-[#1E293B] line-clamp-2 leading-tight mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
{note.title}
</h3>
<p className="text-xs text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>
{note.subject} · Sem {note.semester}
<p className="text-[11px] text-[#64748B] font-medium truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
{note.subject} Sem {note.semester}
</p>
</div>
<div className="flex items-center justify-between text-xs text-[#94A3B8] pt-2 border-t border-[#E5E7EB]">
<span style={{ fontFamily: 'Inter, sans-serif' }}>{note.downloads} downloads · {note.fileSize}</span>
</div>
</div>
{/* Metadata Line */}
<div className="flex items-center justify-between border-t border-[#F1F5F9] pt-3 text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-800">{note.fileSize}</span>
<span className="px-1 py-0.2 bg-[#F1F5F9] rounded text-[9px] font-bold text-slate-600">PDF</span>
</div>
<span>{note.uploadedAt ? `Updated ${note.uploadedAt}` : 'Recently'}</span>
</div>
{/* Download Button */}
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white"
onClick={() => handleDownload(note)}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white cursor-pointer"
style={{
fontFamily: 'Poppins, sans-serif',
background: 'linear-gradient(135deg, #F97316, #FB923C)',

View File

@@ -35,6 +35,16 @@ export interface Faculty {
office?: string;
avatar?: string;
specialization?: string;
qualification?: string;
experience?: string;
subjectsHandling?: string;
researchAreas?: string;
officeHours?: string;
achievements?: string[];
publications?: string[];
studentsGuided?: { ug: number; pg: number; phd: number };
languagesKnown?: string[];
joinedInstitution?: string;
}
// ─── Bus Routes ──────────────────────────────────────────────────────────────
@@ -85,14 +95,31 @@ export interface Event {
}
// ─── Clubs ───────────────────────────────────────────────────────────────────
export interface ClubSocialLinks {
instagram?: string;
linkedin?: string;
whatsapp?: string;
youtube?: string;
github?: string;
website?: string;
}
export interface Club {
id: string;
name: string;
description: string;
details?: string;
category: string;
members: number;
logo?: string;
presidentName?: string;
year?: string;
contactEmail?: string;
contactPhone?: string;
logo?: string;
logoUrl?: string;
icon?: string;
defaultRank?: number;
socialLinks?: ClubSocialLinks;
}
// ─── Community / Q&A ─────────────────────────────────────────────────────────