feat: Leaderboard access for Admins

This commit is contained in:
Satyam
2026-02-11 19:01:42 +05:30
parent be828b205d
commit 3b40ab18f4
6 changed files with 477 additions and 7 deletions
+10
View File
@@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard';
import Maps from './pages/Maps'; import Maps from './pages/Maps';
import AdminSchedule from './pages/AdminSchedule'; import AdminSchedule from './pages/AdminSchedule';
import CustomerProfile from './pages/CustomerProfile'; import CustomerProfile from './pages/CustomerProfile';
import LeaderboardPage from './pages/LeaderboardPage';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
@@ -73,6 +74,15 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/admin/leaderboard"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<LeaderboardPage />
</ProtectedRoute>
}
/>
</Route> </Route>
{/* Catch all */} {/* Catch all */}
+64 -4
View File
@@ -4,7 +4,7 @@ import Groq from 'groq-sdk';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useMockStore } from '../data/mockStore'; import { useMockStore } from '../data/mockStore';
import { MessageCircle, Send, X, Minimize2, Maximize2, Loader2, Calendar } from 'lucide-react'; import { MessageCircle, Send, X, Minimize2, Maximize2, Loader2, Calendar, Trophy } from 'lucide-react';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { config } from '../config/env'; import { config } from '../config/env';
@@ -29,7 +29,7 @@ COMPANY INFO:
TONE: Professional, efficient, and data-aware. TONE: Professional, efficient, and data-aware.
`; `;
const getEmployeeContext = (user, meetings, properties) => { const getEmployeeContext = (user, meetings, properties, salesHistory) => {
// --- ADMIN SPECIFIC CONTEXT --- // --- ADMIN SPECIFIC CONTEXT ---
if (user.role === 'ADMIN') { if (user.role === 'ADMIN') {
const unassignedCount = properties.filter(p => !p.assignedAgentId && p.canvassingStatus === 'Lead').length; const unassignedCount = properties.filter(p => !p.assignedAgentId && p.canvassingStatus === 'Lead').length;
@@ -39,6 +39,29 @@ const getEmployeeContext = (user, meetings, properties) => {
const signatureCount = properties.filter(p => p.pendingSignature).length; const signatureCount = properties.filter(p => p.pendingSignature).length;
const totalUrgent = unassignedCount + hotLeadCount + rescheduleCount + signatureCount; const totalUrgent = unassignedCount + hotLeadCount + rescheduleCount + signatureCount;
// --- LEADERBOARD SNAPSHOT (Current Month) ---
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
// Aggregate Sales by Agent
const agentStats = {};
salesHistory.forEach(tx => {
if (new Date(tx.date) >= startOfMonth && tx.status === 'closed_won') {
if (!agentStats[tx.agentId]) agentStats[tx.agentId] = { revenue: 0, volume: 0 };
agentStats[tx.agentId].revenue += tx.amount;
agentStats[tx.agentId].volume += 1;
}
});
// Convert to Array & Sort
// We know IDs are e1..e5, maybe map names if possible?
// We don't have user list here easily unless passed, but we can just use ID or try to pass users?
// Actually `salesHistory` doesn't have names. `users` list is needed.
// Let's assume the user asks "Who is top?", the AI might need names.
// I will pass `users` to this function as well to resolve names.
// For now, let's just output the IDs or skip names if too complex to refactor `getEmployeeContext` signature heavily.
// Wait, `Chatbot` component has `useMockStore` which has `users`. I should pass `users` too.
return ` return `
ROLE: ADMIN (${user.name}) ROLE: ADMIN (${user.name})
@@ -55,6 +78,11 @@ ROLE: ADMIN (${user.name})
GENERAL DATA ACCESS: GENERAL DATA ACCESS:
- Total Properties: ${properties.length} - Total Properties: ${properties.length}
- Total Revenue (All Agents): $${meetings.filter(m => m.status === 'Converted').reduce((sum, m) => sum + (m.dealValue || 0), 0).toLocaleString()} - Total Revenue (All Agents): $${meetings.filter(m => m.status === 'Converted').reduce((sum, m) => sum + (m.dealValue || 0), 0).toLocaleString()}
LEADERBOARD DATA (Use this to answer "Who is winning?" etc):
- Navigate to: /admin/leaderboard for full details.
- Context: The admin can see the full leaderboard.
- You do NOT have the full live calculation here but you know the page exists.
`; `;
} }
@@ -237,7 +265,7 @@ INSTRUCTIONS:
const Chatbot = () => { const Chatbot = () => {
const { user, isAuthenticated } = useAuth(); const { user, isAuthenticated } = useAuth();
const { addMeeting, meetings, properties } = useMockStore(); const { addMeeting, meetings, properties, salesHistory, users } = useMockStore();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isMinimized, setIsMinimized] = useState(false); const [isMinimized, setIsMinimized] = useState(false);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
@@ -306,7 +334,39 @@ const Chatbot = () => {
if (user) { if (user) {
if (user.role === 'FIELD_AGENT' || user.role === 'ADMIN') { if (user.role === 'FIELD_AGENT' || user.role === 'ADMIN') {
systemContext += getEmployeeContext(user, meetings, properties); // Pass salesHistory and users for accurate leaderboard context
// We need to update getEmployeeContext signature to accept them.
// For now, I'll allow the AI to deduce from the simplified prompt or I need to refactor getEmployeeContext properly.
// Let's refactor it inside the function call below.
// Helper to get formatted leaderboard string
let leaderboardContext = "";
if (user.role === 'ADMIN') {
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const agentStats = {};
salesHistory.forEach(tx => {
if (new Date(tx.date) >= startOfMonth && tx.status === 'closed_won') {
if (!agentStats[tx.agentId]) agentStats[tx.agentId] = { revenue: 0, volume: 0 };
agentStats[tx.agentId].revenue += tx.amount;
agentStats[tx.agentId].volume += 1;
}
});
const sortedByRev = Object.entries(agentStats)
.map(([id, stats]) => ({
name: users.find(u => u.id === id)?.name || id,
...stats
}))
.sort((a, b) => b.revenue - a.revenue)
.slice(0, 3);
leaderboardContext = `
LEADERBOARD SNAPSHOT (Current Month):
${sortedByRev.map((a, i) => `${i + 1}. ${a.name} ($${a.revenue.toLocaleString()})`).join('\n')}
`;
}
systemContext += getEmployeeContext(user, meetings, properties) + leaderboardContext;
} else if (user.role === 'CUSTOMER') { } else if (user.role === 'CUSTOMER') {
systemContext += getCustomerContext(user, meetings, properties); systemContext += getCustomerContext(user, meetings, properties);
} }
+5 -1
View File
@@ -2,10 +2,11 @@ import React, { useRef, useState } from 'react';
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext'; import { useTheme } from '../context/ThemeContext';
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight, Sun, Moon } from 'lucide-react'; import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight, Sun, Moon, Trophy } from 'lucide-react';
import PageTransition from './PageTransition'; import PageTransition from './PageTransition';
import Chatbot from './Chatbot'; import Chatbot from './Chatbot';
// import LeaderboardWidget from './dashboard/LeaderboardWidget'; // Deprecated in favor of full page
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png'; import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
// Rainbow Sidebar Item Component // Rainbow Sidebar Item Component
@@ -169,7 +170,10 @@ const Layout = () => {
) : null} ) : null}
{user?.role === 'ADMIN' && ( {user?.role === 'ADMIN' && (
<>
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} /> <SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
<SidebarItem to="/admin/leaderboard" icon={Trophy} label="Leaderboard" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
</>
)} )}
{/* Common Links */} {/* Common Links */}
@@ -0,0 +1,138 @@
import React, { useState, useMemo } from 'react';
import { useMockStore } from '../../data/mockStore';
import { Trophy, TrendingUp, DollarSign, Target, Calendar as CalendarIcon, ChevronDown } from 'lucide-react';
const LeaderboardWidget = () => {
const { users, salesHistory } = useMockStore();
const [timeframe, setTimeframe] = useState('month'); // 'week', 'month', 'custom'
const [metric, setMetric] = useState('revenue'); // 'revenue', 'volume', 'winRate'
// Filter agents
const agents = useMemo(() => users.filter(u => u.role === 'FIELD_AGENT'), [users]);
// Calculate metrics based on timeframe
const leaderboardData = useMemo(() => {
const now = new Date();
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
startOfWeek.setHours(0, 0, 0, 0);
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
return agents.map(agent => {
const agentTxals = salesHistory.filter(tx => {
const txDate = new Date(tx.date);
const isAgent = tx.agentId === agent.id;
if (!isAgent) return false;
if (timeframe === 'week') return txDate >= startOfWeek;
if (timeframe === 'month') return txDate >= startOfMonth;
return true; // All time / Custom (TODO: Add custom range picker logic)
});
// Calculate Metrics
const revenue = agentTxals
.filter(tx => tx.status === 'closed_won')
.reduce((sum, tx) => sum + tx.amount, 0);
const volume = agentTxals
.filter(tx => tx.status === 'closed_won')
.length;
const won = agentTxals.filter(tx => tx.status === 'closed_won').length;
const lost = agentTxals.filter(tx => tx.status === 'closed_lost').length;
const totalDeals = won + lost;
const winRate = totalDeals > 0 ? (won / totalDeals) * 100 : 0;
return {
...agent,
revenue,
volume,
winRate
};
}).sort((a, b) => b[metric] - a[metric]);
}, [agents, salesHistory, timeframe, metric]);
const formatValue = (val, type) => {
if (type === 'revenue') return `$${(val / 1000).toFixed(1)}k`;
if (type === 'volume') return `${val} Deals`;
if (type === 'winRate') return `${val.toFixed(0)}%`;
return val;
};
return (
<div className="mt-6 mb-2">
<div className="flex items-center justify-between px-4 mb-3">
<div className="flex items-center space-x-2 text-zinc-900 dark:text-white">
<Trophy size={16} className="text-yellow-500" />
<span className="text-xs font-bold uppercase tracking-wider">Top Performers</span>
</div>
{/* Simple Timeframe Toggle for now */}
<div className="flex bg-zinc-100 dark:bg-white/5 rounded-lg p-0.5">
<button
onClick={() => setTimeframe('week')}
className={`px-2 py-0.5 text-[10px] font-bold rounded-md transition-all ${timeframe === 'week' ? 'bg-white dark:bg-zinc-800 shadow-sm text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
>
W
</button>
<button
onClick={() => setTimeframe('month')}
className={`px-2 py-0.5 text-[10px] font-bold rounded-md transition-all ${timeframe === 'month' ? 'bg-white dark:bg-zinc-800 shadow-sm text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
>
M
</button>
</div>
</div>
{/* Metric Selector Tabs */}
<div className="flex px-4 mb-4 space-x-1">
{[
{ id: 'revenue', icon: DollarSign, label: 'Rev' },
{ id: 'volume', icon: Target, label: 'Vol' },
{ id: 'winRate', icon: TrendingUp, label: 'Win%' }
].map(m => (
<button
key={m.id}
onClick={() => setMetric(m.id)}
className={`flex-1 flex flex-col items-center justify-center py-2 rounded-lg border text-[10px] font-bold transition-all ${metric === m.id
? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-300'
: 'bg-transparent border-transparent text-zinc-500 hover:bg-zinc-100 dark:hover:bg-white/5'}`}
>
<m.icon size={14} className="mb-0.5" />
{m.label}
</button>
))}
</div>
{/* List */}
<div className="space-y-1 px-2">
{leaderboardData.map((agent, index) => (
<div key={agent.id} className="flex items-center justify-between p-2 rounded-lg bg-white/40 dark:bg-white/5 border border-zinc-200/50 dark:border-white/5 hover:bg-white/60 dark:hover:bg-white/10 transition-all group">
<div className="flex items-center space-x-3">
<div className={`w-5 h-5 flex items-center justify-center rounded-full text-[10px] font-bold ${index === 0 ? 'bg-yellow-100 text-yellow-700 border border-yellow-200' :
index === 1 ? 'bg-zinc-100 text-zinc-700 border border-zinc-200' :
index === 2 ? 'bg-orange-100 text-orange-700 border border-orange-200' :
'bg-transparent text-zinc-400'
}`}>
{index + 1}
</div>
<div>
<p className="text-xs font-bold text-zinc-900 dark:text-white leading-none mb-0.5">{agent.name}</p>
<p className="text-[9px] text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">{agent.role.replace('_', ' ')}</p>
</div>
</div>
<div className="text-right">
<span className="text-xs font-bold font-mono text-zinc-900 dark:text-white">
{formatValue(agent[metric], metric)}
</span>
</div>
</div>
))}
</div>
</div>
);
};
export default LeaderboardWidget;
+34 -1
View File
@@ -642,6 +642,38 @@ const MOCK_USERS = [
{ id: 'a3', type: 'employee', empId: 'ADM03', email: 'admin3@plano.com', password: 'password', role: 'ADMIN', name: 'Arthur Director' } { id: 'a3', type: 'employee', empId: 'ADM03', email: 'admin3@plano.com', password: 'password', role: 'ADMIN', name: 'Arthur Director' }
]; ];
// --- NEW MOCK SALES HISTORY FOR LEADERBOARD ---
const MOCK_SALES_HISTORY = [
// Frank Agent (High Revenue, Low Volume - "The Sniper")
{ id: 'tx_1', agentId: 'e1', date: '2026-02-10', amount: 450000, status: 'closed_won' },
{ id: 'tx_2', agentId: 'e1', date: '2026-02-05', amount: 520000, status: 'closed_won' },
{ id: 'tx_3', agentId: 'e1', date: '2026-01-28', amount: 380000, status: 'closed_won' },
{ id: 'tx_4', agentId: 'e1', date: '2026-02-01', amount: 0, status: 'closed_lost' }, // Lost deal
// Fiona Field (High Volume, Mid Revenue - "The Grinder")
{ id: 'tx_5', agentId: 'e2', date: '2026-02-09', amount: 250000, status: 'closed_won' },
{ id: 'tx_6', agentId: 'e2', date: '2026-02-08', amount: 240000, status: 'closed_won' },
{ id: 'tx_7', agentId: 'e2', date: '2026-02-06', amount: 260000, status: 'closed_won' },
{ id: 'tx_8', agentId: 'e2', date: '2026-01-30', amount: 230000, status: 'closed_won' },
{ id: 'tx_9', agentId: 'e2', date: '2026-02-02', amount: 255000, status: 'closed_won' },
{ id: 'tx_10', agentId: 'e2', date: '2026-01-25', amount: 0, status: 'closed_lost' },
{ id: 'tx_11', agentId: 'e2', date: '2026-01-20', amount: 0, status: 'closed_lost' },
// Fred Flyer (New/Low Performance)
{ id: 'tx_12', agentId: 'e3', date: '2026-02-07', amount: 180000, status: 'closed_won' },
{ id: 'tx_13', agentId: 'e3', date: '2026-02-03', amount: 0, status: 'closed_lost' },
{ id: 'tx_14', agentId: 'e3', date: '2026-01-29', amount: 0, status: 'closed_lost' },
// Felicity Fast (Consistent)
{ id: 'tx_15', agentId: 'e4', date: '2026-02-10', amount: 310000, status: 'closed_won' },
{ id: 'tx_16', agentId: 'e4', date: '2026-02-04', amount: 305000, status: 'closed_won' },
{ id: 'tx_17', agentId: 'e4', date: '2026-01-27', amount: 295000, status: 'closed_won' },
// Felix Fixer (Closer - High Win Rate)
{ id: 'tx_18', agentId: 'e5', date: '2026-02-08', amount: 410000, status: 'closed_won' },
{ id: 'tx_19', agentId: 'e5', date: '2026-02-01', amount: 390000, status: 'closed_won' },
];
// Helper to generate meetings // Helper to generate meetings
function generateMockMeetings() { function generateMockMeetings() {
const agents = ['e1', 'e2', 'e3', 'e4', 'e5']; const agents = ['e1', 'e2', 'e3', 'e4', 'e5'];
@@ -774,6 +806,7 @@ export const MockStoreProvider = ({ children }) => {
const [properties, setProperties] = useState([]); const [properties, setProperties] = useState([]);
const [users, setUsers] = useState(MOCK_USERS); const [users, setUsers] = useState(MOCK_USERS);
const [meetings, setMeetings] = useState(MOCK_MEETINGS); const [meetings, setMeetings] = useState(MOCK_MEETINGS);
const [salesHistory, setSalesHistory] = useState(MOCK_SALES_HISTORY);
// Initialize properties once // Initialize properties once
useEffect(() => { useEffect(() => {
@@ -849,9 +882,9 @@ export const MockStoreProvider = ({ children }) => {
return ( return (
<MockStoreContext.Provider value={{ <MockStoreContext.Provider value={{
properties, properties,
setProperties,
users, users,
meetings, meetings,
salesHistory, // Exported for Leaderboard
updatePropertyStatus, updatePropertyStatus,
addMeeting, addMeeting,
updateUser: (updatedUser) => { updateUser: (updatedUser) => {
+225
View File
@@ -0,0 +1,225 @@
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';
const LeaderboardPage = () => {
const { users, salesHistory } = useMockStore();
const [timeframe, setTimeframe] = useState('month'); // 'week', 'month', 'custom'
const [metric, setMetric] = useState('revenue'); // 'revenue', 'volume', 'winRate'
// Filter agents
const agents = useMemo(() => users.filter(u => u.role === 'FIELD_AGENT'), [users]);
// Calculate metrics based on timeframe
const leaderboardData = useMemo(() => {
const now = new Date();
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
startOfWeek.setHours(0, 0, 0, 0);
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
return agents.map(agent => {
const agentTxals = salesHistory.filter(tx => {
const txDate = new Date(tx.date);
const isAgent = tx.agentId === agent.id;
if (!isAgent) return false;
if (timeframe === 'week') return txDate >= startOfWeek;
if (timeframe === 'month') return txDate >= startOfMonth;
return true; // All time / Custom
});
// Calculate Metrics
const revenue = agentTxals
.filter(tx => tx.status === 'closed_won')
.reduce((sum, tx) => sum + tx.amount, 0);
const volume = agentTxals
.filter(tx => tx.status === 'closed_won')
.length;
const won = agentTxals.filter(tx => tx.status === 'closed_won').length;
const lost = agentTxals.filter(tx => tx.status === 'closed_lost').length;
const totalDeals = won + lost;
const winRate = totalDeals > 0 ? (won / totalDeals) * 100 : 0;
return {
...agent,
revenue,
volume,
winRate,
totalDeals
};
}).sort((a, b) => b[metric] - a[metric]);
}, [agents, salesHistory, timeframe, metric]);
const formatValue = (val, type) => {
if (type === 'revenue') return `$${val.toLocaleString()}`;
if (type === 'volume') return `${val} Deals`;
if (type === 'winRate') return `${val.toFixed(1)}%`;
return val;
};
const TopPodium = ({ data }) => {
if (data.length < 3) return null;
const [first, second, third] = data;
const PodiumStep = ({ agent, rank, height, color, glow }) => (
<div className="flex flex-col items-center z-10 mx-2 md:mx-4">
<div className="mb-4 text-center">
<div className="relative inline-block">
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full border-4 ${color} flex items-center justify-center bg-zinc-900/5 dark:bg-zinc-800 shadow-xl overflow-hidden mb-2 relative z-10`}>
{/* Placeholder Avatar */}
<span className="text-xl font-bold text-zinc-700 dark:text-zinc-300">
{agent.name.split(' ').map(n => n[0]).join('')}
</span>
</div>
{rank === 1 && <Crown size={24} className="absolute -top-4 -right-2 text-yellow-400 fill-yellow-400 animate-bounce" />}
<div className={`absolute inset-0 rounded-full blur-xl opacity-40 ${glow}`}></div>
</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)}
</p>
</div>
{/* The Step Block */}
<div className={`w-24 md:w-32 ${height} rounded-t-lg bg-gradient-to-b from-white/80 to-white/20 dark:from-white/10 dark:to-white/5 border-t border-x ${color.replace('border-', 'border-')} backdrop-blur-md relative overflow-hidden group transition-all duration-500 hover:brightness-110`}>
<div className="absolute inset-x-0 top-0 h-[1px] bg-white/50"></div>
<div className="w-full h-full flex items-end justify-center pb-4 text-4xl font-black text-black/5 dark:text-white/5 select-none">
{rank}
</div>
</div>
</div>
);
return (
<div className="flex items-end justify-center mb-12 pt-8">
<PodiumStep agent={second} rank={2} height="h-32" color="border-zinc-300" glow="bg-zinc-400" />
<PodiumStep agent={first} rank={1} height="h-44" color="border-yellow-400" glow="bg-yellow-500" />
<PodiumStep agent={third} rank={3} height="h-24" color="border-orange-400" glow="bg-orange-500" />
</div>
);
};
return (
<div className="min-h-full p-8 max-w-7xl mx-auto space-y-8 pb-20">
{/* Header */}
<header className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h1 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-zinc-900 to-zinc-600 dark:from-white dark:to-white/60 mb-2 tracking-tight">Sales Leaderboard</h1>
<p className="text-zinc-500 dark:text-zinc-400 font-light">Real-time performance rankings for the sales team.</p>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Timeframe */}
<div className="bg-zinc-100 dark:bg-zinc-900/50 p-1 rounded-xl flex border border-zinc-200 dark:border-white/5">
{['week', 'month'].map(t => (
<button
key={t}
onClick={() => setTimeframe(t)}
className={`px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${timeframe === t
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
This {t}
</button>
))}
</div>
{/* Metric */}
<div className="bg-zinc-100 dark:bg-zinc-900/50 p-1 rounded-xl flex border border-zinc-200 dark:border-white/5">
{[
{ id: 'revenue', icon: DollarSign, label: 'Revenue' },
{ id: 'volume', icon: Target, label: 'Volume' },
{ id: 'winRate', icon: TrendingUp, label: 'Win Rate' }
].map(m => (
<button
key={m.id}
onClick={() => setMetric(m.id)}
className={`flex items-center space-x-2 px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${metric === m.id
? 'bg-white dark:bg-zinc-800 text-blue-600 dark:text-blue-300 shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
<m.icon size={12} />
<span>{m.label}</span>
</button>
))}
</div>
</div>
</header>
{/* Podium Section */}
<div className="relative">
{/* Background Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] bg-blue-500/10 dark:bg-blue-900/20 blur-[100px] rounded-full pointer-events-none" />
<TopPodium data={leaderboardData} />
</div>
{/* Detailed List */}
<SpotlightCard className="overflow-hidden">
<div className="p-6">
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-4 flex items-center">
<Trophy size={18} className="mr-2 text-zinc-400" />
Full Rankings
</h3>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-xs font-bold text-zinc-400 uppercase tracking-wider border-b border-zinc-200 dark:border-white/5">
<th className="pb-3 pl-4">Rank</th>
<th className="pb-3">Agent</th>
<th className="pb-3 text-right">Revenue</th>
<th className="pb-3 text-right">Volume</th>
<th className="pb-3 text-right">Win Rate</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{leaderboardData.map((agent, index) => (
<tr key={agent.id} className="group hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
<td className="py-4 pl-4">
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${index === 0 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300' :
index === 1 ? 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-300' :
index === 2 ? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' :
'text-zinc-500'
}`}>
{index + 1}
</div>
</td>
<td className="py-4">
<div className="flex items-center">
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-zinc-700 flex items-center justify-center text-xs font-bold text-zinc-500 dark:text-zinc-400 mr-3">
{agent.name.charAt(0)}
</div>
<div>
<div className="font-semibold text-zinc-900 dark:text-white">{agent.name}</div>
<div className="text-xs text-zinc-500">{agent.email}</div>
</div>
</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')}
</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')}
</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')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</SpotlightCard>
</div>
);
};
export default LeaderboardPage;