feat: Add GSAP animated counters and fix Team Schedule theme issues

- Created reusable AnimatedNumber component with GSAP
- Integrated animated counters in Dashboard (metrics, weather)
- Integrated animated counters in Leaderboard (podium, table)
- Fixed AdminSchedule dark mode styling with theme-aware classes
- Added responsive columns and functional dropdown to Team Schedule
- Implemented date range and status filters for Team Schedule
- Improved animation smoothness with expo.out easing
This commit is contained in:
Satyam
2026-02-16 13:48:04 +05:30
parent e0da94aa0c
commit ed79b266af
4 changed files with 471 additions and 69 deletions
+116
View File
@@ -0,0 +1,116 @@
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
/**
* AnimatedNumber - A reusable component that animates numbers from 0 to their target value using GSAP
* @param {number|string} value - The target value to animate to
* @param {number} duration - Animation duration in seconds (default: 2)
* @param {string} prefix - Optional prefix (e.g., "$")
* @param {string} suffix - Optional suffix (e.g., "%", "K", "M")
* @param {number} decimals - Number of decimal places (default: 0)
* @param {boolean} useLocaleString - Whether to format with commas (default: false)
* @param {string} className - Additional CSS classes
*/
const AnimatedNumber = ({
value,
duration = 2,
prefix = '',
suffix = '',
decimals = 0,
useLocaleString = false,
className = '',
delay = 0
}) => {
const numberRef = useRef(null);
const animationRef = useRef(null);
useEffect(() => {
if (!numberRef.current) return;
// Extract numeric value if it's a string like "$4.2M"
let numericValue = value;
let extractedPrefix = prefix;
let extractedSuffix = suffix;
if (typeof value === 'string') {
// Check for currency symbols
if (value.startsWith('$')) {
extractedPrefix = '$';
value = value.substring(1);
}
// Check for suffixes like M, K, B
const lastChar = value.slice(-1);
if (['K', 'M', 'B'].includes(lastChar.toUpperCase())) {
extractedSuffix = lastChar.toUpperCase();
value = value.slice(0, -1);
}
// Parse the numeric part
numericValue = parseFloat(value.replace(/,/g, ''));
}
// Handle non-numeric values
if (isNaN(numericValue)) {
numberRef.current.textContent = value;
return;
}
// Convert suffix multipliers to actual numbers
let multiplier = 1;
if (extractedSuffix === 'K') multiplier = 1000;
if (extractedSuffix === 'M') multiplier = 1000000;
if (extractedSuffix === 'B') multiplier = 1000000000;
const targetValue = numericValue * multiplier;
// Animate from 0 to target value
const obj = { val: 0 };
animationRef.current = gsap.to(obj, {
val: numericValue,
duration: duration,
delay: delay,
ease: 'expo.out',
onUpdate: () => {
if (numberRef.current) {
let displayValue = obj.val.toFixed(decimals);
if (useLocaleString) {
displayValue = parseFloat(displayValue).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`;
}
},
onComplete: () => {
// Ensure final value is exact
if (numberRef.current) {
let displayValue = numericValue.toFixed(decimals);
if (useLocaleString) {
displayValue = parseFloat(displayValue).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`;
}
}
});
return () => {
if (animationRef.current) {
animationRef.current.kill();
}
};
}, [value, duration, prefix, suffix, decimals, useLocaleString, delay]);
return <span ref={numberRef} className={className}>0</span>;
};
export default AnimatedNumber;
+319 -39
View File
@@ -1,82 +1,362 @@
import React from 'react';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
import { Calendar, User, Clock, MapPin, MoreHorizontal } from 'lucide-react';
import { useTheme } from '../context/ThemeContext';
import { Calendar, User, Clock, MapPin, MoreHorizontal, Filter, X, ChevronDown } from 'lucide-react';
const AdminSchedule = () => {
const { meetings } = useMockStore();
// In a real app we would have a function to update meetings here
const { theme } = useTheme();
// Group meetings by agent for display
const meetingsByAgent = meetings.reduce((acc, meeting) => {
const agentId = meeting.agentId;
if (!acc[agentId]) acc[agentId] = [];
acc[agentId].push(meeting);
return acc;
}, {});
// Refs for click-outside detection
const dateDropdownRef = useRef(null);
const statusDropdownRef = useRef(null);
// Filter states
const [dateFilter, setDateFilter] = useState('all'); // all, today, week, month, quarter, custom
const [customDateRange, setCustomDateRange] = useState({ start: '', end: '' });
const [statusFilter, setStatusFilter] = useState([]);
const [showDateDropdown, setShowDateDropdown] = useState(false);
const [showStatusDropdown, setShowStatusDropdown] = useState(false);
const [openDropdownId, setOpenDropdownId] = useState(null);
// Click-outside detection for filter dropdowns
useEffect(() => {
const handleClickOutside = (event) => {
if (dateDropdownRef.current && !dateDropdownRef.current.contains(event.target)) {
setShowDateDropdown(false);
}
if (statusDropdownRef.current && !statusDropdownRef.current.contains(event.target)) {
setShowStatusDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Click-outside detection for action dropdowns
useEffect(() => {
const handleClickOutside = (event) => {
if (openDropdownId && !event.target.closest('.action-dropdown-container')) {
setOpenDropdownId(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [openDropdownId]);
// Get unique statuses from meetings
const availableStatuses = useMemo(() => {
return [...new Set(meetings.map(m => m.status))];
}, [meetings]);
// Filter meetings based on selected filters
const filteredMeetings = useMemo(() => {
let filtered = [...meetings];
// Date filtering
if (dateFilter !== 'all') {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
filtered = filtered.filter(meeting => {
const meetingDate = new Date(meeting.date);
switch (dateFilter) {
case 'today':
return meetingDate.toDateString() === today.toDateString();
case 'week':
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
return meetingDate >= weekStart && meetingDate <= weekEnd;
case 'month':
return meetingDate.getMonth() === now.getMonth() &&
meetingDate.getFullYear() === now.getFullYear();
case 'quarter':
const quarter = Math.floor(now.getMonth() / 3);
const meetingQuarter = Math.floor(meetingDate.getMonth() / 3);
return meetingQuarter === quarter &&
meetingDate.getFullYear() === now.getFullYear();
case 'custom':
if (customDateRange.start && customDateRange.end) {
const start = new Date(customDateRange.start);
const end = new Date(customDateRange.end);
return meetingDate >= start && meetingDate <= end;
}
return true;
default:
return true;
}
});
}
// Status filtering
if (statusFilter.length > 0) {
filtered = filtered.filter(meeting => statusFilter.includes(meeting.status));
}
return filtered;
}, [meetings, dateFilter, customDateRange, statusFilter]);
const toggleStatusFilter = (status) => {
setStatusFilter(prev =>
prev.includes(status)
? prev.filter(s => s !== status)
: [...prev, status]
);
};
const clearFilters = () => {
setDateFilter('all');
setCustomDateRange({ start: '', end: '' });
setStatusFilter([]);
};
const handleActionClick = (meetingId, action) => {
console.log(`Action ${action} for meeting ${meetingId}`);
// TODO: Implement actual actions
setOpenDropdownId(null);
};
return (
<div className="p-8 max-w-7xl mx-auto">
<header className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Team Schedule</h1>
<p className="text-slate-400">Manage field agent appointments</p>
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white mb-2">Team Schedule</h1>
<p className="text-zinc-600 dark:text-slate-400">Manage field agent appointments</p>
</header>
<div className="bg-slate-900 border border-slate-800 rounded-3xl shadow-xl overflow-hidden">
<div className="p-6 border-b border-slate-800">
<h2 className="text-xl font-bold text-white flex items-center">
{/* Filters Section */}
<div className="mb-6 flex flex-wrap gap-4 items-center">
{/* Date Filter */}
<div className="relative" ref={dateDropdownRef}>
<button
onClick={() => setShowDateDropdown(!showDateDropdown)}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg text-sm font-medium text-zinc-700 dark:text-slate-200 hover:bg-zinc-50 dark:hover:bg-slate-700 transition-colors"
>
<Calendar size={16} />
{dateFilter === 'all' ? 'All Dates' :
dateFilter === 'today' ? 'Today' :
dateFilter === 'week' ? 'This Week' :
dateFilter === 'month' ? 'This Month' :
dateFilter === 'quarter' ? 'This Quarter' :
'Custom Range'}
<ChevronDown size={16} />
</button>
{showDateDropdown && (
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[200px]">
<button
onClick={() => { setDateFilter('all'); setShowDateDropdown(false); }}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
All Dates
</button>
<button
onClick={() => { setDateFilter('today'); setShowDateDropdown(false); }}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
Today
</button>
<button
onClick={() => { setDateFilter('week'); setShowDateDropdown(false); }}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
This Week
</button>
<button
onClick={() => { setDateFilter('month'); setShowDateDropdown(false); }}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
This Month
</button>
<button
onClick={() => { setDateFilter('quarter'); setShowDateDropdown(false); }}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
This Quarter
</button>
<div className="border-t border-zinc-200 dark:border-slate-700 p-3">
<p className="text-xs font-semibold text-zinc-500 dark:text-slate-400 mb-2">Custom Range</p>
<input
type="date"
value={customDateRange.start}
onChange={(e) => setCustomDateRange(prev => ({ ...prev, start: e.target.value }))}
className="w-full px-2 py-1 text-xs bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-600 rounded mb-2 text-zinc-900 dark:text-white"
placeholder="Start date"
/>
<input
type="date"
value={customDateRange.end}
onChange={(e) => setCustomDateRange(prev => ({ ...prev, end: e.target.value }))}
className="w-full px-2 py-1 text-xs bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-600 rounded mb-2 text-zinc-900 dark:text-white"
placeholder="End date"
/>
<button
onClick={() => { setDateFilter('custom'); setShowDateDropdown(false); }}
disabled={!customDateRange.start || !customDateRange.end}
className="w-full px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Apply
</button>
</div>
</div>
)}
</div>
{/* Status Filter */}
<div className="relative" ref={statusDropdownRef}>
<button
onClick={() => setShowStatusDropdown(!showStatusDropdown)}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg text-sm font-medium text-zinc-700 dark:text-slate-200 hover:bg-zinc-50 dark:hover:bg-slate-700 transition-colors"
>
<Filter size={16} />
Status {statusFilter.length > 0 && `(${statusFilter.length})`}
<ChevronDown size={16} />
</button>
{showStatusDropdown && (
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[200px]">
{availableStatuses.map(status => (
<label
key={status}
className="flex items-center gap-2 px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 cursor-pointer transition-colors"
>
<input
type="checkbox"
checked={statusFilter.includes(status)}
onChange={() => toggleStatusFilter(status)}
className="rounded border-zinc-300 dark:border-slate-600"
/>
{status}
</label>
))}
</div>
)}
</div>
{/* Clear Filters */}
{(dateFilter !== 'all' || statusFilter.length > 0) && (
<button
onClick={clearFilters}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
>
<X size={16} />
Clear Filters
</button>
)}
{/* Results Count */}
<div className="ml-auto text-sm text-zinc-600 dark:text-slate-400">
Showing {filteredMeetings.length} of {meetings.length} appointments
</div>
</div>
<div className="bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-800 rounded-3xl shadow-xl overflow-hidden">
<div className="p-6 border-b border-zinc-200 dark:border-slate-800">
<h2 className="text-xl font-bold text-zinc-900 dark:text-white flex items-center">
<Calendar size={20} className="text-blue-500 mr-2" />
All Active Appointments
</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm text-slate-400">
<thead className="bg-slate-950 text-slate-200 uppercase font-bold text-xs">
<table className="w-full text-left text-sm text-zinc-600 dark:text-slate-400">
<thead className="bg-zinc-100 dark:bg-slate-950 text-zinc-700 dark:text-slate-200 uppercase font-bold text-xs">
<tr>
<th className="px-6 py-4">Agent</th>
<th className="px-6 py-4">Customer</th>
<th className="px-6 py-4">Property</th>
<th className="px-6 py-4">Date & Time</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4 text-right">Actions</th>
<th className="px-6 py-4 min-w-[150px]">Agent</th>
<th className="px-6 py-4 min-w-[180px]">Customer</th>
<th className="px-6 py-4 min-w-[120px]">Property</th>
<th className="px-6 py-4 min-w-[160px]">Date & Time</th>
<th className="px-6 py-4 min-w-[120px]">Status</th>
<th className="px-6 py-4 text-right min-w-[150px]">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{meetings.map((meeting) => (
<tr key={meeting.id} className="hover:bg-slate-800/50 transition-colors">
<tbody className="divide-y divide-zinc-200 dark:divide-slate-800">
{filteredMeetings.length === 0 ? (
<tr>
<td colSpan="6" className="px-6 py-12 text-center text-zinc-500 dark:text-slate-400">
No appointments found matching the selected filters.
</td>
</tr>
) : (
filteredMeetings.map((meeting) => (
<tr key={meeting.id} className="hover:bg-zinc-50 dark:hover:bg-slate-800/50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center text-slate-300 font-bold text-xs">
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-slate-700 flex items-center justify-center text-zinc-700 dark:text-slate-300 font-bold text-xs">
{meeting.agentId}
</div>
<span className="font-medium text-slate-200">Agent {meeting.agentId}</span>
<span className="font-medium text-zinc-900 dark:text-slate-200 whitespace-normal">Agent {meeting.agentId}</span>
</div>
</td>
<td className="px-6 py-4 text-slate-300">{meeting.customerName}</td>
<td className="px-6 py-4">{meeting.propertyId}</td>
<td className="px-6 py-4 text-zinc-900 dark:text-slate-300 whitespace-normal">{meeting.customerName}</td>
<td className="px-6 py-4 text-zinc-700 dark:text-slate-400">{meeting.propertyId}</td>
<td className="px-6 py-4">
<div className="flex flex-col">
<span className="text-slate-200">{meeting.date}</span>
<span className="text-xs">{meeting.time}</span>
<span className="text-zinc-900 dark:text-slate-200">{meeting.date}</span>
<span className="text-xs text-zinc-600 dark:text-slate-400">{meeting.time}</span>
</div>
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 text-xs font-semibold rounded ${meeting.status === 'Completed' ? 'bg-green-500/20 text-green-400' :
meeting.status === 'Scheduled' ? 'bg-blue-500/20 text-blue-400' :
'bg-yellow-500/20 text-yellow-400'
<span className={`px-2 py-1 text-xs font-semibold rounded whitespace-nowrap ${meeting.status === 'Completed' ? 'bg-green-500/20 text-green-600 dark:text-green-400' :
meeting.status === 'Scheduled' ? 'bg-blue-500/20 text-blue-600 dark:text-blue-400' :
meeting.status === 'Converted' ? 'bg-purple-500/20 text-purple-600 dark:text-purple-400' :
meeting.status === 'Cancelled' ? 'bg-red-500/20 text-red-600 dark:text-red-400' :
'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400'
}`}>
{meeting.status}
</span>
</td>
<td className="px-6 py-4 text-right">
<button className="text-blue-400 hover:text-blue-300 text-xs font-bold mr-3">Reschedule</button>
<button className="text-slate-600 hover:text-slate-400">
<div className="flex items-center justify-end gap-3">
<button className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 text-xs font-bold">
Reschedule
</button>
<div className="relative">
<button
onClick={() => setOpenDropdownId(openDropdownId === meeting.id ? null : meeting.id)}
className="text-zinc-600 dark:text-slate-400 hover:text-zinc-900 dark:hover:text-slate-200 transition-colors"
>
<MoreHorizontal size={16} />
</button>
{openDropdownId === meeting.id && (
<div className="absolute right-0 top-full mt-1 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[160px]">
<button
onClick={() => handleActionClick(meeting.id, 'view')}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
View Details
</button>
<button
onClick={() => handleActionClick(meeting.id, 'reschedule')}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
Reschedule
</button>
<button
onClick={() => handleActionClick(meeting.id, 'complete')}
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
>
Mark Complete
</button>
<button
onClick={() => handleActionClick(meeting.id, 'cancel')}
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
Cancel
</button>
</div>
)}
</div>
</div>
</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
+8 -5
View File
@@ -16,6 +16,7 @@ import { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighbor
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
import { ActionCenterWidget } from '../components/dashboard/ActionCenterWidget';
import { WeatherRiskGauge } from '../components/dashboard/WeatherRiskGauge';
import AnimatedNumber from '../components/AnimatedNumber';
import Loader from '../components/Loader';
@@ -360,7 +361,9 @@ const MetricCard = ({ title, value, trend, icon: Icon, trendColor, bgGlow }) =>
<span className={`text-[10px] uppercase font-bold tracking-wider ${trendColor} bg-zinc-100 dark:bg-white/5 px-2.5 py-1 rounded-full border border-zinc-200 dark:border-white/5 backdrop-blur-md`}>{trend}</span>
</div>
<div>
<h3 className="text-3xl font-extrabold text-zinc-900 dark:text-white mb-2 tracking-tight drop-shadow-sm">{value}</h3>
<h3 className="text-3xl font-extrabold text-zinc-900 dark:text-white mb-2 tracking-tight drop-shadow-sm">
<AnimatedNumber value={value} duration={1.5} useLocaleString={typeof value === 'number'} />
</h3>
<p className="text-zinc-500 dark:text-zinc-500 text-xs font-semibold uppercase tracking-widest">{title}</p>
</div>
@@ -432,7 +435,7 @@ const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onR
</div>
<h2 className="text-5xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-b from-zinc-800 to-zinc-400 dark:from-white dark:to-white/50 flex items-start">
{Math.round(weather.main.temp)}
<AnimatedNumber value={Math.round(weather.main.temp)} duration={1.8} />
<span className="text-2xl text-zinc-400 dark:text-white/50 mt-2 font-bold tracking-normal">°F</span>
</h2>
<p className="text-[10px] text-zinc-400 mt-1 font-mono">
@@ -452,14 +455,14 @@ const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onR
<Wind size={14} className="mr-2" />
<span className="text-[10px] uppercase font-bold tracking-wider">Wind</span>
</div>
<span className="text-2xl font-bold">{Math.round(weather.wind.speed)} <span className="text-sm font-normal text-zinc-500">mph</span></span>
<span className="text-2xl font-bold"><AnimatedNumber value={Math.round(weather.wind.speed)} duration={1.6} /> <span className="text-sm font-normal text-zinc-500">mph</span></span>
</div>
<div className="bg-zinc-100 dark:bg-white/5 rounded-2xl p-4 border border-zinc-200 dark:border-white/5 backdrop-blur-sm">
<div className="flex items-center text-zinc-500 dark:text-zinc-400 mb-2">
<Droplets size={14} className="mr-2" />
<span className="text-[10px] uppercase font-bold tracking-wider">Humidity</span>
</div>
<span className="text-2xl font-bold">{weather.main.humidity}<span className="text-sm font-normal text-zinc-500">%</span></span>
<span className="text-2xl font-bold"><AnimatedNumber value={weather.main.humidity} duration={1.6} /><span className="text-sm font-normal text-zinc-500">%</span></span>
</div>
</div>
@@ -475,7 +478,7 @@ const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onR
<div className="my-2 opacity-70 group-hover:opacity-100 transition-opacity">
{getMiniIcon(item.weather[0].id)}
</div>
<span className="text-sm font-bold text-zinc-700 dark:text-zinc-300">{Math.round(item.main.temp)}°</span>
<span className="text-sm font-bold text-zinc-700 dark:text-zinc-300"><AnimatedNumber value={Math.round(item.main.temp)} duration={1.4} delay={index * 0.15} />°</span>
</div>
)
})}
+7 -4
View File
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
import { useMockStore } from '../data/mockStore';
import { Trophy, TrendingUp, DollarSign, Target, Calendar, Medal, ArrowUpRight, Crown } from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard';
import AnimatedNumber from '../components/AnimatedNumber';
const LeaderboardPage = () => {
const { users, salesHistory } = useMockStore();
@@ -82,7 +83,9 @@ const LeaderboardPage = () => {
</div>
<h3 className="font-bold text-sm md:text-base text-zinc-900 dark:text-white truncate max-w-[100px]">{agent.name}</h3>
<p className={`font-mono font-bold text-sm md:text-lg ${color.replace('border-', 'text-')}`}>
{formatValue(agent[metric], metric)}
{metric === 'revenue' && <AnimatedNumber value={agent[metric]} prefix="$" useLocaleString duration={1.5} delay={rank * 0.2} />}
{metric === 'volume' && <><AnimatedNumber value={agent[metric]} duration={1.5} delay={rank * 0.2} /> Deals</>}
{metric === 'winRate' && <><AnimatedNumber value={agent[metric]} decimals={1} duration={1.5} delay={rank * 0.2} />%</>}
</p>
</div>
@@ -203,13 +206,13 @@ const LeaderboardPage = () => {
</div>
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'revenue' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
{formatValue(agent.revenue, 'revenue')}
<AnimatedNumber value={agent.revenue} prefix="$" useLocaleString duration={1.2} delay={index * 0.05} />
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'volume' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
{formatValue(agent.volume, 'volume')}
<AnimatedNumber value={agent.volume} duration={1.2} delay={index * 0.05} /> Deals
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'winRate' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
{formatValue(agent.winRate, 'winRate')}
<AnimatedNumber value={agent.winRate} decimals={1} duration={1.2} delay={index * 0.05} />%
</td>
</tr>
))}