feat: initial frontend build for RIT Freshers Hub
This commit is contained in:
97
src/components/AnimatedContainer/AnimatedContainer.tsx
Normal file
97
src/components/AnimatedContainer/AnimatedContainer.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface AnimatedContainerProps {
|
||||
children: ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
direction?: 'up' | 'left' | 'right' | 'fade';
|
||||
}
|
||||
|
||||
const variants = {
|
||||
up: {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
},
|
||||
left: {
|
||||
hidden: { opacity: 0, x: -30 },
|
||||
visible: { opacity: 1, x: 0 },
|
||||
},
|
||||
right: {
|
||||
hidden: { opacity: 0, x: 30 },
|
||||
visible: { opacity: 1, x: 0 },
|
||||
},
|
||||
fade: {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
export default function AnimatedContainer({
|
||||
children,
|
||||
delay = 0,
|
||||
className = '',
|
||||
direction = 'up',
|
||||
}: AnimatedContainerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{ duration: 0.55, delay, ease: 'easeOut' }}
|
||||
variants={variants[direction]}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Stagger container for grid cards
|
||||
export function StaggerContainer({
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
},
|
||||
},
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Individual stagger item
|
||||
export function StaggerItem({
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: 'easeOut' } },
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
114
src/components/BusCard/BusCard.tsx
Normal file
114
src/components/BusCard/BusCard.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { Clock, MapPin, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { BusRoute } from '@/types';
|
||||
|
||||
interface BusCardProps {
|
||||
route: BusRoute;
|
||||
}
|
||||
|
||||
export default function BusCard({ route }: BusCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
whileHover={{ y: -3 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-white border border-[#E5E7EB] rounded-2xl overflow-hidden"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Color Strip */}
|
||||
<div className="h-1.5" style={{ backgroundColor: route.color }} />
|
||||
|
||||
<div className="p-5">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span
|
||||
className="px-2.5 py-0.5 rounded-full text-xs font-bold text-white"
|
||||
style={{ backgroundColor: route.color, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{route.number}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-[#1E293B] text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{route.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-[#94A3B8] shrink-0">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{route.departureTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route Summary */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full border-2" style={{ borderColor: route.color }} />
|
||||
<div className="w-0.5 h-8 bg-[#E5E7EB]" />
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: route.color }} />
|
||||
</div>
|
||||
<div className="flex flex-col justify-between h-12">
|
||||
<span className="text-xs font-medium text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{route.from}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{route.to}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrival */}
|
||||
<div className="flex items-center justify-between text-xs text-[#94A3B8] mb-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{route.stops.length} stops</span>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>Arrives: {route.arrivalTime}</span>
|
||||
</div>
|
||||
|
||||
{/* Toggle Stops */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
color: route.color,
|
||||
backgroundColor: `${route.color}12`,
|
||||
}}
|
||||
>
|
||||
{expanded ? 'Hide Stops' : 'View Stops'}
|
||||
{expanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
|
||||
{/* Stops Expanded */}
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: expanded ? 'auto' : 0, opacity: expanded ? 1 : 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-3 flex flex-col gap-2 pl-1">
|
||||
{route.stops.map((stop, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: i === 0 || i === route.stops.length - 1 ? route.color : '#E5E7EB' }}
|
||||
/>
|
||||
{i < route.stops.length - 1 && <div className="w-0.5 h-4 bg-[#E5E7EB]" />}
|
||||
</div>
|
||||
<div className="flex items-center justify-between flex-1">
|
||||
<span className="text-xs text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>{stop.name}</span>
|
||||
<span className="text-xs text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>{stop.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
127
src/components/ChatPreview/ChatPreview.tsx
Normal file
127
src/components/ChatPreview/ChatPreview.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { Bot, Sparkles, ArrowRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AI_SUGGESTED_PROMPTS } from '@/constants';
|
||||
|
||||
const PREVIEW_MESSAGES = [
|
||||
{ role: 'user', text: 'What documents do I need for the first day?' },
|
||||
{ role: 'ai', text: 'For your first day at RIT, you\'ll need: Allotment order, 10th & 12th mark sheets, Transfer Certificate, Conduct Certificate, and 10 passport-size photos. Originals and 2 sets of photocopies are required. Would you like a complete checklist?' },
|
||||
{ role: 'user', text: 'Yes, and what about hostel admission?' },
|
||||
{ role: 'ai', text: 'For hostel admission, additionally bring your Aadhaar card, medical fitness certificate, and filled hostel application form. The hostel fee can be paid at the accounts office.' },
|
||||
];
|
||||
|
||||
export default function ChatPreview() {
|
||||
return (
|
||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden" style={{ boxShadow: '0 8px 40px -8px rgba(0,0,0,0.12)' }}>
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-[#E5E7EB] flex items-center gap-3">
|
||||
<div
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center"
|
||||
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
|
||||
>
|
||||
<Bot className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
RIT AI Assistant
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-[#94A3B8]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block" />
|
||||
Powered by Gemini AI
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<span className="px-2.5 py-1 rounded-full text-xs font-medium bg-[#FFF7ED] text-[#F97316]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="p-5 flex flex-col gap-3 min-h-[260px]">
|
||||
{PREVIEW_MESSAGES.map((msg, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.15 }}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
{msg.role === 'ai' && (
|
||||
<div className="w-7 h-7 rounded-full flex items-center justify-center mr-2 shrink-0 mt-0.5"
|
||||
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
|
||||
<Bot className="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="text-xs leading-relaxed max-w-[78%] px-3.5 py-2.5"
|
||||
style={{
|
||||
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #FB923C)' : '#F8FAFC',
|
||||
color: msg.role === 'user' ? 'white' : '#1E293B',
|
||||
borderRadius: msg.role === 'user' ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
|
||||
border: msg.role === 'ai' ? '1px solid #E5E7EB' : 'none',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
}}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Typing indicator */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div className="flex items-center gap-1 px-4 py-2.5 bg-[#F8FAFC] rounded-2xl border border-[#E5E7EB]">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 bg-[#94A3B8] rounded-full"
|
||||
animate={{ y: [0, -4, 0] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay: i * 0.15 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Suggested Prompts */}
|
||||
<div className="px-5 pb-2 flex flex-wrap gap-2">
|
||||
{AI_SUGGESTED_PROMPTS.slice(0, 3).map((prompt, i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
className="text-xs px-3 py-1.5 rounded-full border border-[#E5E7EB] text-[#475569] cursor-pointer hover:border-[#F97316] hover:text-[#F97316] transition-all"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{prompt}
|
||||
</motion.span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="p-5 pt-3">
|
||||
<Link to="/ai-assistant">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold text-white transition-all"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||
boxShadow: '0 4px 15px rgba(249,115,22,0.35)',
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Ask Anything
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
src/components/FacultyCard/FacultyCard.tsx
Normal file
96
src/components/FacultyCard/FacultyCard.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { Mail, Phone, Building2, User } from 'lucide-react';
|
||||
import type { Faculty } from '@/types';
|
||||
|
||||
interface FacultyCardProps {
|
||||
faculty: Faculty;
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
['#F97316', '#FB923C'],
|
||||
['#8B5CF6', '#A78BFA'],
|
||||
['#10B981', '#34D399'],
|
||||
['#3B82F6', '#60A5FA'],
|
||||
['#EC4899', '#F472B6'],
|
||||
['#F59E0B', '#FCD34D'],
|
||||
];
|
||||
|
||||
export default function FacultyCard({ faculty }: FacultyCardProps) {
|
||||
const colorIndex = parseInt(faculty.id) % AVATAR_COLORS.length;
|
||||
const [from, to] = AVATAR_COLORS[colorIndex];
|
||||
const initials = faculty.name
|
||||
.split(' ')
|
||||
.filter((_, i) => i === 0 || i === faculty.name.split(' ').length - 1)
|
||||
.map((n) => n[0])
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -5, boxShadow: '0 16px 40px -8px rgba(0,0,0,0.12)' }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="bg-white border border-[#E5E7EB] rounded-2xl p-5 flex flex-col gap-4"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Avatar + Name */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0"
|
||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[#1E293B] text-sm leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{faculty.name}
|
||||
</h3>
|
||||
<p className="text-xs text-[#F97316] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.designation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-[#475569]">
|
||||
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
|
||||
</div>
|
||||
{faculty.office && (
|
||||
<div className="flex items-center gap-2 text-xs text-[#475569]">
|
||||
<User className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office}</span>
|
||||
</div>
|
||||
)}
|
||||
{faculty.specialization && (
|
||||
<div className="text-xs text-[#94A3B8] pl-5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.specialization}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB]">
|
||||
<a
|
||||
href={`mailto:${faculty.email}`}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
|
||||
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
|
||||
aria-label={`Email ${faculty.name}`}
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
Email
|
||||
</a>
|
||||
{faculty.phone && (
|
||||
<a
|
||||
href={`tel:${faculty.phone}`}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
aria-label={`Call ${faculty.name}`}
|
||||
>
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
Call
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
63
src/components/FeatureCard/FeatureCard.tsx
Normal file
63
src/components/FeatureCard/FeatureCard.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { Feature } from '@/types';
|
||||
|
||||
interface FeatureCardProps {
|
||||
feature: Feature;
|
||||
}
|
||||
|
||||
export default function FeatureCard({ feature }: FeatureCardProps) {
|
||||
const IconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -6, boxShadow: '0 20px 50px -12px rgba(0,0,0,0.15)' }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="bg-white border border-[#E5E7EB] rounded-2xl p-6 flex flex-col gap-4 group cursor-pointer"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300 group-hover:scale-110"
|
||||
style={{ backgroundColor: feature.bgColor }}
|
||||
>
|
||||
{IconComponent && <IconComponent className="w-6 h-6" style={{ color: feature.color } as React.CSSProperties} />}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1">
|
||||
<h3
|
||||
className="text-base font-semibold text-[#1E293B] mb-1.5"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[#94A3B8] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<Link
|
||||
to={feature.path}
|
||||
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 w-fit"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
color: feature.color,
|
||||
}}
|
||||
aria-label={`Explore ${feature.title}`}
|
||||
>
|
||||
Explore
|
||||
<motion.span
|
||||
className="inline-flex"
|
||||
animate={{ x: [0, 3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
28
src/components/FloatingCard/FloatingCard.tsx
Normal file
28
src/components/FloatingCard/FloatingCard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface FloatingCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default function FloatingCard({ children, className = '', delay = 0, style }: FloatingCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6, delay }}
|
||||
className={`bg-white rounded-2xl border border-[#E5E7EB] shadow-[0_8px_32px_rgba(0,0,0,0.12)] ${className}`}
|
||||
style={style}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, -8, 0] }}
|
||||
transition={{ duration: 3.5, repeat: Infinity, ease: 'easeInOut', delay }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
163
src/components/Footer/Footer.tsx
Normal file
163
src/components/Footer/Footer.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GraduationCap, MapPin, Phone, Mail, Camera, Send, Globe, Play, ArrowRight } from 'lucide-react';
|
||||
|
||||
const quickLinks = [
|
||||
{ label: 'Home', path: '/' },
|
||||
{ label: 'Notes & PYQs', path: '/notes' },
|
||||
{ label: 'AI Assistant', path: '/ai-assistant' },
|
||||
{ label: 'Campus Map', path: '/campus' },
|
||||
{ label: 'Events', path: '/events' },
|
||||
{ label: 'Community', path: '/community' },
|
||||
];
|
||||
|
||||
const resources = [
|
||||
{ label: 'Student Handbook', path: '#' },
|
||||
{ label: 'Academic Calendar', path: '#' },
|
||||
{ label: 'ERP Portal', path: '#' },
|
||||
{ label: 'Library Portal', path: '#' },
|
||||
{ label: 'Exam Results', path: '#' },
|
||||
{ label: 'Fee Payment', path: '#' },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#1E293B] text-white">
|
||||
{/* CTA Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }} className="py-12">
|
||||
<div className="container-custom flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-white" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
Ready to explore RIT?
|
||||
</h3>
|
||||
<p className="text-orange-100 mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Everything a fresher needs — all in one place.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/ai-assistant"
|
||||
className="flex items-center gap-2 bg-white text-[#F97316] px-6 py-3 rounded-xl font-semibold transition-all hover:shadow-lg hover:-translate-y-0.5"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
Ask AI Assistant
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Footer */}
|
||||
<div className="container-custom py-14">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5 mb-4">
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
||||
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
|
||||
>
|
||||
<GraduationCap className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-white text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Freshers Hub</div>
|
||||
<div className="text-[10px] text-slate-400">Rajalakshmi Institute of Technology</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm leading-relaxed mb-5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Your all-in-one guide to campus life at RIT. Notes, AI assistant, events, faculty, and more — made for freshers.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{[Camera, Send, Globe, Play].map((Icon, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href="#"
|
||||
className="w-9 h-9 rounded-lg bg-slate-700 flex items-center justify-center text-slate-400 hover:bg-[#F97316] hover:text-white transition-all duration-200"
|
||||
aria-label="Social link"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Quick Links
|
||||
</h4>
|
||||
<ul className="space-y-2.5">
|
||||
{quickLinks.map((link) => (
|
||||
<li key={link.path}>
|
||||
<Link
|
||||
to={link.path}
|
||||
className="text-slate-400 hover:text-[#F97316] text-sm transition-colors flex items-center gap-1.5 group"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[#F97316] opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Resources
|
||||
</h4>
|
||||
<ul className="space-y-2.5">
|
||||
{resources.map((res) => (
|
||||
<li key={res.label}>
|
||||
<a
|
||||
href={res.path}
|
||||
className="text-slate-400 hover:text-[#F97316] text-sm transition-colors flex items-center gap-1.5 group"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[#F97316] opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
{res.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Contact
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3 text-slate-400 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
<MapPin className="w-4 h-4 mt-0.5 shrink-0 text-[#F97316]" />
|
||||
<span>Rajalakshmi Institute of Technology,<br />Kuthambakkam, Chennai - 600 124</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-3 text-slate-400 text-sm">
|
||||
<Phone className="w-4 h-4 shrink-0 text-[#F97316]" />
|
||||
<a href="tel:+914423422890" className="hover:text-[#F97316] transition-colors">+91 44 2342 2890</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-3 text-slate-400 text-sm">
|
||||
<Mail className="w-4 h-4 shrink-0 text-[#F97316]" />
|
||||
<a href="mailto:info@rit.ac.in" className="hover:text-[#F97316] transition-colors">info@rit.ac.in</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-slate-700">
|
||||
<div className="container-custom py-5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<p className="text-slate-500 text-xs" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
© 2025 RIT Freshers Hub. All rights reserved.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-slate-500">
|
||||
<a href="#" className="hover:text-[#F97316] transition-colors">Privacy Policy</a>
|
||||
<span>·</span>
|
||||
<a href="#" className="hover:text-[#F97316] transition-colors">Terms of Use</a>
|
||||
<span>·</span>
|
||||
<a href="#" className="hover:text-[#F97316] transition-colors">Accessibility</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
181
src/components/Navbar/Navbar.tsx
Normal file
181
src/components/Navbar/Navbar.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Menu, X, GraduationCap, Bell } from 'lucide-react';
|
||||
import { NAV_LINKS } from '@/constants';
|
||||
|
||||
export default function Navbar() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setIsScrolled(window.scrollY > 20);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobileOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.nav
|
||||
initial={{ y: -100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
className={`fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-6xl transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'shadow-[0_8px_32px_rgba(0,0,0,0.12)] bg-white/90'
|
||||
: 'bg-white/80'
|
||||
}`}
|
||||
style={{
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
borderRadius: '20px',
|
||||
border: '1px solid rgba(229,231,235,0.8)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-2.5">
|
||||
{/* Logo - RIT Event Hub Style */}
|
||||
<Link to="/" className="flex items-center gap-2 group">
|
||||
{/* Blue Circular Tree Icon */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center shrink-0"
|
||||
style={{ background: 'linear-gradient(135deg, #1E40AF, #3B82F6)' }}
|
||||
>
|
||||
<svg viewBox="0 0 100 100" className="w-6.5 h-6.5 text-white" fill="none" stroke="currentColor" strokeWidth="6" strokeLinecap="round">
|
||||
{/* Minimalist Tree Branch Representation */}
|
||||
<path d="M50 85 V40" />
|
||||
<path d="M50 65 Q65 55 75 60" />
|
||||
<path d="M50 55 Q35 45 25 50" />
|
||||
<path d="M50 45 Q70 30 65 15" />
|
||||
<path d="M50 45 Q30 30 35 15" />
|
||||
<circle cx="50" cy="38" r="8" fill="white" stroke="none" />
|
||||
<circle cx="65" cy="15" r="5" fill="white" stroke="none" />
|
||||
<circle cx="35" cy="15" r="5" fill="white" stroke="none" />
|
||||
<circle cx="75" cy="60" r="5" fill="white" stroke="none" />
|
||||
<circle cx="25" cy="50" r="5" fill="white" stroke="none" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<span className="font-extrabold text-[#1E293B] text-2xl tracking-tighter" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
rit
|
||||
</span>
|
||||
<div className="h-7 w-[1px] bg-gray-300 mx-2" />
|
||||
<div className="flex flex-col text-[8.5px] font-bold text-[#475569] leading-tight tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
<span>RAJALAKSHMI</span>
|
||||
<span>INSTITUTE OF</span>
|
||||
<span>TECHNOLOGY</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = location.pathname === link.path;
|
||||
return (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="relative px-4 py-2 rounded-xl text-xs font-semibold uppercase tracking-wider transition-all duration-200"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
color: isActive ? '#F97316' : '#475569',
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-active"
|
||||
className="absolute inset-0 rounded-xl"
|
||||
style={{ backgroundColor: '#FFF7ED' }}
|
||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.4 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">{link.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Right Side */}
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="hidden md:flex w-9 h-9 rounded-xl items-center justify-center text-[#94A3B8] hover:text-[#F97316] hover:bg-[#FFF7ED] transition-all"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<Bell className="w-4.5 h-4.5" />
|
||||
</motion.button>
|
||||
|
||||
{/* Profile styled as an orange LOG OUT pill button from Reference Image */}
|
||||
<Link to="/profile" className="hidden md:block">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03, boxShadow: '0 4px 15px rgba(249,115,22,0.3)' }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className="px-5 py-2 rounded-full text-white font-bold text-[10.5px] tracking-wider uppercase transition-all"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||
}}
|
||||
>
|
||||
Profile
|
||||
</motion.button>
|
||||
</Link>
|
||||
|
||||
{/* Mobile Toggle */}
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
||||
className="md:hidden w-9 h-9 rounded-xl flex items-center justify-center text-[#475569] hover:bg-gray-100 transition-all"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isMobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<AnimatePresence>
|
||||
{isMobileOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="overflow-hidden md:hidden"
|
||||
>
|
||||
<div className="px-4 pb-4 pt-2 flex flex-col gap-1 border-t border-gray-100">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = location.pathname === link.path;
|
||||
return (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="px-4 py-3 rounded-xl text-sm font-medium transition-all"
|
||||
style={{
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
color: isActive ? '#F97316' : '#475569',
|
||||
backgroundColor: isActive ? '#FFF7ED' : 'transparent',
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.nav>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="h-20" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
82
src/components/SectionTitle/SectionTitle.tsx
Normal file
82
src/components/SectionTitle/SectionTitle.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface SectionTitleProps {
|
||||
tag?: string;
|
||||
title: string;
|
||||
highlight?: string;
|
||||
subtitle?: string;
|
||||
align?: 'left' | 'center';
|
||||
}
|
||||
|
||||
export default function SectionTitle({
|
||||
tag,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
align = 'center',
|
||||
}: SectionTitleProps) {
|
||||
const isCenter = align === 'center';
|
||||
|
||||
return (
|
||||
<div className={`mb-12 ${isCenter ? 'text-center' : 'text-left'}`}>
|
||||
{tag && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className={`inline-flex items-center gap-2 mb-3 ${isCenter ? 'mx-auto' : ''}`}
|
||||
>
|
||||
<span
|
||||
className="px-3 py-1 rounded-full text-xs font-semibold tracking-wider uppercase"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
backgroundColor: '#FFF7ED',
|
||||
color: '#F97316',
|
||||
border: '1px solid #FED7AA',
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
className="text-3xl md:text-4xl font-bold"
|
||||
style={{ fontFamily: 'Playfair Display, serif', color: '#1E293B' }}
|
||||
>
|
||||
{title}{' '}
|
||||
{highlight && (
|
||||
<span
|
||||
className="relative inline-block"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
{highlight}
|
||||
</span>
|
||||
)}
|
||||
</motion.h2>
|
||||
|
||||
{subtitle && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className={`mt-3 text-base text-[#475569] max-w-xl ${isCenter ? 'mx-auto' : ''}`}
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
src/components/StatsCard/StatsCard.tsx
Normal file
80
src/components/StatsCard/StatsCard.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { Stat } from '@/types';
|
||||
|
||||
interface StatsCardProps {
|
||||
stat: Stat;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
function useCountUp(target: number, duration: number = 1800, start: boolean = false) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!start) return;
|
||||
let startTime: number | null = null;
|
||||
const step = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - progress, 3); // ease-out cubic
|
||||
setCount(Math.floor(ease * target));
|
||||
if (progress < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
}, [target, duration, start]);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export default function StatsCard({ stat, delay = 0 }: StatsCardProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const count = useCountUp(stat.value, 1800, isVisible);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
);
|
||||
if (ref.current) observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
whileHover={{ y: -3, boxShadow: '0 8px 30px rgba(0,0,0,0.06)' }}
|
||||
className="relative bg-white border border-[#E5E7EB] rounded-2xl p-6 py-8 flex flex-col items-center justify-center text-center group overflow-hidden"
|
||||
style={{ boxShadow: '0 2px 10px rgba(0,0,0,0.03)' }}
|
||||
>
|
||||
{/* Corner Bracket Decorations from Reference Image */}
|
||||
<div className="absolute top-4 right-4 w-3.5 h-3.5 border-t-2 border-r-2 border-[#F97316]/50 rounded-tr-[4px] transition-all duration-300 group-hover:scale-110 group-hover:border-[#F97316]" />
|
||||
<div className="absolute bottom-4 left-4 w-3.5 h-3.5 border-b-2 border-l-2 border-[#F97316]/50 rounded-bl-[4px] transition-all duration-300 group-hover:scale-110 group-hover:border-[#F97316]" />
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="text-3xl md:text-4xl font-extrabold text-[#1E293B] tracking-tight"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{count}
|
||||
{stat.suffix}
|
||||
</div>
|
||||
<div
|
||||
className="text-[11px] font-bold uppercase tracking-wider text-[#94A3B8] mt-2"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user