Skip to content

Commit

Permalink
feat: implement leaderboard with dynamic data and custom styling
Browse files Browse the repository at this point in the history
  • Loading branch information
harshaldulera committed Dec 7, 2024
1 parent 40cf8b4 commit b387038
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 124 deletions.
3 changes: 2 additions & 1 deletion packages/nextjs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ yarn-error.log*
.env.production.local

# typescript
*.tsbuildinfo
*.tsbuildinfo

182 changes: 117 additions & 65 deletions packages/nextjs/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,113 +5,165 @@ import { LEADERBOARD } from "./queries";
import { useQuery } from "@apollo/client";
import { motion } from "framer-motion";

interface LeaderboardEntry {
rank: number;
interface Transfer {
blockTimestamp: string;
blockNumber: string;
from: string;
id: string;
tokenId: string;
transactionHash: string;
to: string;
}

interface TransferCounts {
address: string;
score: number;
eth: number;
count: number;
tokenIds: string[];
}

export default function Leaderboard() {
const { data, loading, error } = useQuery(LEADERBOARD);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
console.log(data);

const leaderboardData: LeaderboardEntry[] = [
{ rank: 1, address: "0x1234...5678", score: 1500, eth: 2.5 },
{ rank: 2, address: "0x8765...4321", score: 1450, eth: 2.45 },
{ rank: 3, address: "0x9876...1234", score: 1400, eth: 2.4 },
{ rank: 4, address: "0x5432...8765", score: 1350, eth: 2.35 },
{ rank: 5, address: "0x2468...1357", score: 1300, eth: 2.3 },
];
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="animate-pulse text-neon-green text-2xl">Loading Leaderboard...</div>
</div>
);
}

// Generate additional entries with predictable values
const extendedData = [...leaderboardData];
for (let i = leaderboardData.length + 1; i <= 50; i++) {
extendedData.push({
rank: i,
address: `0x${i.toString().padStart(4, "0")}...${(1000 - i).toString().padStart(4, "0")}`,
score: Math.max(0, 1500 - i * 20),
eth: Number((2.5 - i * 0.05).toFixed(2)),
});
if (error) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-error text-xl">Error: {error.message}</div>
</div>
);
}

// Process transfers data to create leaderboard
const addressCounts = data.transfers.reduce((acc: { [key: string]: TransferCounts }, transfer: Transfer) => {
// Count both sending and receiving
[transfer.from, transfer.to].forEach(address => {
if (address !== "0x0000000000000000000000000000000000000000") { // Exclude zero address
if (!acc[address]) {
acc[address] = {
address,
count: 0,
tokenIds: []
};
}
acc[address].count += 1;
if (!acc[address].tokenIds.includes(transfer.tokenId)) {
acc[address].tokenIds.push(transfer.tokenId);
}
}
});
return acc;
}, {});

// Convert to array and sort by count
const leaderboardData = Object.values(addressCounts)
.sort((a, b) => b.count - a.count)
.map((item, index) => ({
...item,
rank: index + 1
}));

return (
<div className="flex flex-col h-screen">
{/* Fixed Hero Section */}
<div className="bg-base-300 py-8">
{/* Hero Section */}
<div className="bg-dark-surface py-8">
<div className="max-w-2xl mx-auto text-center">
<h1 className="text-4xl font-bold mb-4 text-white">Top Stakers</h1>
<p className="text-base-content">
The highest stakers on our platform are showcased here. Join them by staking your ETH!
<h1 className="text-4xl font-bold mb-4 text-neon-green glow-text">Top NFT Traders</h1>
<p className="text-gray-300">
Most active addresses in NFT transfers on our platform
</p>
</div>
</div>

{/* Scrollable Leaderboard Section */}
{/* Leaderboard Section */}
<div className="flex-grow overflow-hidden">
<div className="max-w-2xl mx-auto p-6 h-full">
{/* Sticky Header */}
<div className="sticky top-0 bg-base-100 p-4 rounded-lg mb-3 z-10 shadow-lg">
<div className="flex justify-between text-sm text-base-content">
<div className="sticky top-0 bg-darker-surface p-4 rounded-lg mb-3 z-10 shadow-neon-glow">
<div className="flex justify-between text-sm text-gray-300">
<span>Rank & Address</span>
<div className="flex gap-8 pr-4">
<span>Score</span>
<span>Staked</span>
<div className="flex gap-8">
<span>Transfers</span>
<span>NFTs</span>
</div>
</div>
</div>

{/* Scrollable Content */}
<div className="space-y-3 overflow-y-auto h-[calc(100vh-400px)] pr-2 custom-scrollbar">
{extendedData.map((entry, index) => (
{leaderboardData.map((entry, index) => (
<motion.div
key={entry.rank}
key={entry.address}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className={`p-4 rounded-lg ${
entry.rank === 1
? "bg-yellow-500/20 border border-yellow-500/50"
: entry.rank === 2
? "bg-gray-400/20 border border-gray-400/50"
: entry.rank === 3
? "bg-orange-600/20 border border-orange-600/50"
: "bg-base-200"
} hover:bg-base-300 transition-all duration-200 transform hover:scale-[1.02]`}
entry.rank === 1 ? 'bg-dark-surface border border-neon-green shadow-neon-glow' :
entry.rank === 2 ? 'bg-dark-surface border border-gray-500' :
entry.rank === 3 ? 'bg-dark-surface border border-orange-600' :
'bg-dark-surface'
} hover:bg-medium-surface transition-all duration-200 transform hover:scale-[1.02]`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center ${
entry.rank === 1
? "bg-yellow-500"
: entry.rank === 2
? "bg-gray-400"
: entry.rank === 3
? "bg-orange-600"
: "bg-base-300"
}`}
>
<span className="font-bold text-base-100">{entry.rank}</span>
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
entry.rank === 1 ? 'bg-neon-green text-black' :
entry.rank === 2 ? 'bg-gray-500' :
entry.rank === 3 ? 'bg-orange-600' :
'bg-medium-surface'
}`}>
<span className="font-bold">{entry.rank}</span>
</div>
<span className="text-white font-mono">{entry.address}</span>
<span className="text-gray-300 font-mono">
{entry.address.slice(0, 6)}...{entry.address.slice(-4)}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col items-end">
<span className=" font-bold text-gray-400">{entry.score}</span>
</div>
<div className="flex flex-col items-end min-w-[80px]">
<span className="text-green-500 font-bold">{entry.eth} ETH</span>
</div>
<div className="flex items-center gap-8">
<span className={`font-bold ${
entry.rank === 1 ? 'text-neon-green' : 'text-gray-300'
}`}>
{entry.count}
</span>
<span className="text-gray-300">
{entry.tokenIds.length}
</span>
</div>
</div>
</motion.div>
))}
</div>
</div>
</div>

{/* Stats Section */}
<div className="bg-dark-surface py-6">
<div className="max-w-2xl mx-auto grid grid-cols-3 gap-4 text-center">
<div>
<h3 className="text-2xl font-bold text-neon-green glow-text">
{leaderboardData.length}
</h3>
<p className="text-gray-300">Total Traders</p>
</div>
<div>
<h3 className="text-2xl font-bold text-neon-green glow-text">
{data.transfers.length}
</h3>
<p className="text-gray-300">Total Transfers</p>
</div>
<div>
<h3 className="text-2xl font-bold text-neon-green glow-text">
{new Set(data.transfers.map((t: Transfer) => t.tokenId)).size}
</h3>
<p className="text-gray-300">Unique NFTs</p>
</div>
</div>
</div>
</div>
);
}
}
12 changes: 8 additions & 4 deletions packages/nextjs/app/leaderboard/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { gql } from "@apollo/client";

export const LEADERBOARD = gql`
query LeaderBoard {
userStakes(orderBy: totalStaked, orderDirection: desc) {
totalStaked
user
transfers {
blockTimestamp
blockNumber
from
id
tokenId
transactionHash
to
}
}
`;
`;
96 changes: 42 additions & 54 deletions packages/nextjs/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,59 @@ module.exports = {
"./utils/**/*.{js,ts,jsx,tsx}",
],
plugins: [require("daisyui")],
darkTheme: "dark",
darkMode: ["selector", "[data-theme='dark']"],
darkTheme: "fitpal",
darkMode: ["selector", "[data-theme='fitpal']"],
daisyui: {
themes: [
{
light: {
primary: '#000001',
'primary-content': '#fbf8fe',
secondary: '#2d2c2e',
'secondary-content': '#fbf8fe',
accent: '#b6b7bb',
'accent-content': '#000001',
neutral: '#a3a2a7',
'neutral-content': '#fbf8fe',
'base-100': '#fbf8fe',
'base-200': '#2d2c2e',
'base-300': '#000001',
'base-content': '#000001',
info: '#b6b7bb',
success: '#34EEB6',
warning: '#FFCF72',
error: '#FF8863',
'--rounded-btn': '9999rem',
'.tooltip': { '--tooltip-tail': '6px' },
'.link': { textUnderlineOffset: '2px' },
'.link:hover': { opacity: '80%' }
},
},
{
dark: {
primary: '#000001', // Full Black
'primary-content': '#fbf8fe', // White
secondary: '#2d2c2e', // Med Grey
'secondary-content': '#fbf8fe', // White
accent: '#b6b7bb', // Inner Grey
'accent-content': '#fbf8fe', // White
neutral: '#a3a2a7', // Mid Grey
'neutral-content': '#fbf8fe', // White
'base-100': '#2d2c2e', // Med Grey
'base-200': '#000001', // Full Black
'base-300': '#2d2c2e', // Med Grey
'base-content': '#fbf8fe', // White
info: '#b6b7bb', // Inner Grey
success: '#34EEB6',
warning: '#FFCF72',
error: '#FF8863',
'--rounded-btn': '9999rem',
'.tooltip': { '--tooltip-tail': '6px', '--tooltip-color': 'oklch(var(--p))' },
'.link': { textUnderlineOffset: '2px' },
'.link:hover': { opacity: '80%' }
fitpal: {
primary: '#9FFF5B', // Neon Green
'primary-content': '#000000',
secondary: '#1C1C1E', // Dark Grey
'secondary-content': '#ffffff',
accent: '#9FFF5B', // Neon Green
'accent-content': '#000000',
neutral: '#2C2C2E', // Medium Grey
'neutral-content': '#ffffff',
'base-100': '#000000', // Pure Black
'base-200': '#1C1C1E', // Dark Grey
'base-300': '#2C2C2E', // Medium Grey
'base-content': '#ffffff',
info: '#9FFF5B', // Neon Green
success: '#9FFF5B', // Neon Green
warning: '#FFD426', // Warning Yellow
error: '#FF4545', // Error Red

// Custom properties
'--rounded-btn': '0.75rem',
'--border-btn': '1px',
'.glass': {
'background': 'rgba(28,28,30,0.8)',
'backdrop-filter': 'blur(8px)',
},
'.chart-line': {
'stroke': '#9FFF5B',
'filter': 'drop-shadow(0 0 2px #9FFF5B)',
},
},
},
],
},
theme: {
extend: {
colors: {
'full-black': '#000001',
'med-grey': '#2d2c2e',
'inner-grey': '#b6b7bb',
'mid-grey': '#a3a2a7',
'pure-white': '#fbf8fe',
'neon-green': '#9FFF5B',
'dark-surface': '#1C1C1E',
'darker-surface': '#000000',
'medium-surface': '#2C2C2E',
},
boxShadow: {
'neon-glow': '0 0 10px rgba(159, 255, 91, 0.3)',
'neon-glow-strong': '0 0 15px rgba(159, 255, 91, 0.5)',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
},
boxShadow: { center: '0 0 12px -2px rgb(0 0 0 / 0.05)' },
animation: { 'pulse-fast': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite' }
},
},
};

0 comments on commit b387038

Please sign in to comment.