implement leaderboard ui
This commit is contained in:
parent
d976f13a04
commit
8abb460815
114
app/src/components/Leaderboard.tsx
Normal file
114
app/src/components/Leaderboard.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import styles from '../styles/Leaderboard.module.css'
|
||||
import { TopEarnersResponse, TopRaidersResponse } from '../types/leaderboard'
|
||||
import { formatUnits } from 'viem'
|
||||
|
||||
const SUBGRAPH_URL = 'https://api.studio.thegraph.com/query/75782/slay-the-moloch-base-sepolia/version/latest'
|
||||
|
||||
const Leaderboard = () => {
|
||||
const [topEarners, setTopEarners] = useState<TopEarnersResponse>()
|
||||
const [topRaiders, setTopRaiders] = useState<TopRaidersResponse>()
|
||||
const [activeTab, setActiveTab] = useState<'earners' | 'raiders'>('earners')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLeaderboards = async () => {
|
||||
try {
|
||||
// Fetch top earners
|
||||
const earnersResponse = await fetch(SUBGRAPH_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `{
|
||||
armies(first: 10, orderBy: profitPerSecond, orderDirection: desc) {
|
||||
player {
|
||||
id
|
||||
totalMinted
|
||||
currentBalance
|
||||
numberOfRaids
|
||||
}
|
||||
profitPerSecond
|
||||
molochDenierLevel
|
||||
apprenticeLevel
|
||||
anointedLevel
|
||||
championLevel
|
||||
}
|
||||
}`
|
||||
})
|
||||
})
|
||||
const earnersData = await earnersResponse.json()
|
||||
setTopEarners({ armies: earnersData.data.armies })
|
||||
|
||||
// Fetch top raiders
|
||||
const raidersResponse = await fetch(SUBGRAPH_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `{
|
||||
players(first: 10, orderBy: numberOfRaids, orderDirection: desc) {
|
||||
id
|
||||
numberOfRaids
|
||||
totalMinted
|
||||
currentBalance
|
||||
}
|
||||
}`
|
||||
})
|
||||
})
|
||||
const raidersData = await raidersResponse.json()
|
||||
setTopRaiders({ players: raidersData.data.players })
|
||||
} catch (error) {
|
||||
console.error('Error fetching leaderboard:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchLeaderboards()
|
||||
const interval = setInterval(fetchLeaderboards, 30000) // Refresh every 30 seconds
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.leaderboard}>
|
||||
<h2 className={styles.title}>Leaderboard</h2>
|
||||
|
||||
<div className={styles.tabs}>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'earners' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('earners')}
|
||||
>
|
||||
Top Earners
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'raiders' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('raiders')}
|
||||
>
|
||||
Top Raiders
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'earners' && (
|
||||
<div className={styles.list}>
|
||||
{topEarners?.armies.map((army, index) => (
|
||||
<div key={army.player.id} className={styles.item}>
|
||||
<span className={styles.rank}>#{index + 1}</span>
|
||||
<span className={styles.address}>{army.player.id.slice(0, 6)}...{army.player.id.slice(-4)}</span>
|
||||
<span className={styles.stat}>{formatUnits(BigInt(army.profitPerSecond), 4)} GELD/s</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'raiders' && (
|
||||
<div className={styles.list}>
|
||||
{topRaiders?.players.map((player, index) => (
|
||||
<div key={player.id} className={styles.item}>
|
||||
<span className={styles.rank}>#{index + 1}</span>
|
||||
<span className={styles.address}>{player.id.slice(0, 6)}...{player.id.slice(-4)}</span>
|
||||
<span className={styles.stat}>{player.numberOfRaids} raids</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Leaderboard
|
||||
@ -1,13 +1,16 @@
|
||||
import React, { useCallback } from "react"
|
||||
import React, { useCallback, useState } from "react"
|
||||
import styles from '../styles/Background.module.css';
|
||||
import Tower from "./Tower";
|
||||
import Army from "./Army";
|
||||
import MarchingBand from "./MarchingBand";
|
||||
import MusicPlayer from "./MusicPlayer";
|
||||
import Leaderboard from "./Leaderboard";
|
||||
import { usePlayer } from "../providers/PlayerProvider";
|
||||
|
||||
const Scene = () => {
|
||||
const { isRegistered } = usePlayer();
|
||||
const [isLeaderboardOpen, setIsLeaderboardOpen] = useState(false);
|
||||
|
||||
const handleMusicReady = useCallback((unmute: () => void) => {
|
||||
if (isRegistered) {
|
||||
unmute();
|
||||
@ -25,6 +28,26 @@ const Scene = () => {
|
||||
<div className={`${styles.bonfire} ${styles.background_asset}`} />
|
||||
<Army />
|
||||
<MusicPlayer onReady={handleMusicReady} />
|
||||
<button
|
||||
onClick={() => setIsLeaderboardOpen(true)}
|
||||
className={styles.leaderboardButton}
|
||||
title="Leaderboard"
|
||||
>
|
||||
📜
|
||||
</button>
|
||||
{isLeaderboardOpen && (
|
||||
<div className={styles.leaderboardOverlay}>
|
||||
<div className={styles.leaderboardContent}>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={() => setIsLeaderboardOpen(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<Leaderboard />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@ -256,3 +256,69 @@
|
||||
transform: scale(1.02, 1.03) skew(0deg, -1deg);
|
||||
}
|
||||
}
|
||||
|
||||
.leaderboardButton {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 30px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
transition: all 0.2s cubic-bezier(0.265, 1.4, 0.68, 1.65);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.leaderboardButton:hover {
|
||||
transform: scale(1.1);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.leaderboardButton:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.leaderboardOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.leaderboardContent {
|
||||
background: var(--bg-color);
|
||||
border-width: 8px;
|
||||
border-image: url("/background/frame.png") 22 fill / auto space;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
70
app/src/styles/Leaderboard.module.css
Normal file
70
app/src/styles/Leaderboard.module.css
Normal file
@ -0,0 +1,70 @@
|
||||
.leaderboard {
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 1px solid #666;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: var(--hover-color);
|
||||
border-color: var(--hover-color);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.rank {
|
||||
min-width: 30px;
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.address {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.stat {
|
||||
color: #0f0;
|
||||
font-family: monospace;
|
||||
}
|
||||
25
app/src/types/leaderboard.ts
Normal file
25
app/src/types/leaderboard.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface Player {
|
||||
id: string
|
||||
totalMinted: string
|
||||
currentBalance: string
|
||||
numberOfRaids: string
|
||||
army?: Army
|
||||
}
|
||||
|
||||
export interface Army {
|
||||
player: Player
|
||||
profitPerSecond: string
|
||||
projectedDailyEarnings: string
|
||||
molochDenierLevel: string
|
||||
apprenticeLevel: string
|
||||
anointedLevel: string
|
||||
championLevel: string
|
||||
}
|
||||
|
||||
export interface TopEarnersResponse {
|
||||
armies: Army[]
|
||||
}
|
||||
|
||||
export interface TopRaidersResponse {
|
||||
players: Player[]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user