feat: integrate all frontend modules and premium UI
This commit is contained in:
@@ -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) }}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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;
|
||||
});
|
||||
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 filteredRoutes = BUS_ROUTES.filter((r) =>
|
||||
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 matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
||||
return matchSearch && matchDept;
|
||||
});
|
||||
|
||||
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,8 +73,18 @@ export default function Campus() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{/* Header */}
|
||||
<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">
|
||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
|
||||
@@ -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,52 +226,156 @@ export default function Campus() {
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{filteredRoutes.map((route) => (
|
||||
<StaggerItem key={route.id}>
|
||||
<BusCard route={route} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
|
||||
{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}>
|
||||
<BusCard route={route} />
|
||||
</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>
|
||||
<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' }}
|
||||
>
|
||||
{DEPARTMENTS.map((dept) => (
|
||||
<option key={dept} value={dept}>{dept}</option>
|
||||
))}
|
||||
</select>
|
||||
<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="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) => {
|
||||
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>
|
||||
<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>
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredFaculty.map((faculty) => (
|
||||
<StaggerItem key={faculty.id}>
|
||||
<FacultyCard faculty={faculty} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
<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)' }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-semibold text-[#1E293B] mb-1.5 hover:text-[#F97316] cursor-pointer transition-colors"
|
||||
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">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{q.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-medium bg-[#F8FAFC] text-[#94A3B8] border border-[#E5E7EB]"
|
||||
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">
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
{q.answers} answers
|
||||
</span>
|
||||
{q.isAnswered && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-600 text-[10px] font-semibold">✓ Answered</span>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
</motion.div>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
))}
|
||||
</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: -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">
|
||||
{/* 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>
|
||||
|
||||
<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-[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: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
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-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" />
|
||||
<span>{getAnswersCount(q)} answers</span>
|
||||
</button>
|
||||
{q.isAnswered && (
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -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 */}
|
||||
<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)' }}
|
||||
>
|
||||
<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>
|
||||
{/* ─── Feature 1: "Find My Ideal Club" Banner ────────────────────────── */}
|
||||
<AnimatedContainer className="mb-14">
|
||||
<div
|
||||
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)' }}
|
||||
>
|
||||
{/* 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)' }}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
<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' }}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
<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"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
backgroundColor: selectedCategory === cat ? '#F97316' : '#F8FAFC',
|
||||
color: selectedCategory === cat ? 'white' : '#475569',
|
||||
border: `1px solid ${selectedCategory === cat ? '#F97316' : '#E5E7EB'}`,
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* 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);
|
||||
|
||||
<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>
|
||||
</div>
|
||||
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;
|
||||
|
||||
{/* 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}>
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -4 }}
|
||||
className="bg-white rounded-2xl border border-[#E5E7EB] p-5"
|
||||
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 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>
|
||||
{/* 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={{
|
||||
background: `linear-gradient(135deg, ${categoryColor}, ${categoryColor}DD)`,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,303 +1,215 @@
|
||||
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)' }}
|
||||
/>
|
||||
<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
|
||||
className="container-custom relative z-20 flex flex-col items-center justify-center text-center px-4"
|
||||
style={{ opacity: heroOpacity, y: heroY }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-3"
|
||||
>
|
||||
<span className="text-[#F97316] text-xs font-bold uppercase tracking-widest" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
OUR LEGACY & FUTURE
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<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">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.7 }}
|
||||
className="relative w-full max-w-lg"
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
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
|
||||
</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.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, 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.7, delay: 0.4 }}
|
||||
className="flex flex-wrap justify-center gap-4 mb-8"
|
||||
>
|
||||
<a href="#built-for-freshers">
|
||||
<motion.button
|
||||
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)',
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Explore Hub
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</motion.button>
|
||||
</a>
|
||||
|
||||
{/* 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 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-3"
|
||||
<Link to="/ai-assistant">
|
||||
<motion.button
|
||||
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' }}
|
||||
>
|
||||
<span className="text-[#F97316] text-xs font-bold uppercase tracking-widest" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
OUR LEGACY & FUTURE
|
||||
</span>
|
||||
</motion.div>
|
||||
<Bot className="w-4 h-4" />
|
||||
Ask AI Assistant
|
||||
</motion.button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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' }}
|
||||
>
|
||||
Welcome to <br />
|
||||
<span className="font-serif italic text-[#F97316] font-medium block mt-1">
|
||||
RIT Freshers Hub
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
<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"
|
||||
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.
|
||||
</motion.p>
|
||||
|
||||
<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"
|
||||
>
|
||||
<Link to="/notes">
|
||||
<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"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||
}}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Explore Hub
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</motion.button>
|
||||
</Link>
|
||||
|
||||
<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"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
Ask AI Assistant
|
||||
</motion.button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Badges positioned cleanly below buttons with more breathing room */}
|
||||
<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"
|
||||
>
|
||||
{[
|
||||
{ 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" />
|
||||
<span>{badge.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Badges */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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-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>
|
||||
</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>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
{/* 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>
|
||||
))}
|
||||
</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 */}
|
||||
|
||||
@@ -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-[9px] font-bold uppercase tracking-wider"
|
||||
style={{ backgroundColor: typeConfig.bg, color: typeConfig.text, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{note.type}
|
||||
</span>
|
||||
</div>
|
||||
<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-[11px] text-[#64748B] font-medium truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{note.subject} • Sem {note.semester}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
||||
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' }}>
|
||||
{note.title}
|
||||
</h3>
|
||||
<p className="text-xs text-[#94A3B8]" 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>
|
||||
|
||||
{/* 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)',
|
||||
|
||||
Reference in New Issue
Block a user