1
0
forked from mico/idle_moloch

implement leaderboard ui

This commit is contained in:
san 2024-10-30 19:55:28 +05:30
parent d976f13a04
commit 8abb460815
5 changed files with 299 additions and 1 deletions

View 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

View File

@ -1,13 +1,16 @@
import React, { useCallback } from "react" import React, { useCallback, useState } from "react"
import styles from '../styles/Background.module.css'; import styles from '../styles/Background.module.css';
import Tower from "./Tower"; import Tower from "./Tower";
import Army from "./Army"; import Army from "./Army";
import MarchingBand from "./MarchingBand"; import MarchingBand from "./MarchingBand";
import MusicPlayer from "./MusicPlayer"; import MusicPlayer from "./MusicPlayer";
import Leaderboard from "./Leaderboard";
import { usePlayer } from "../providers/PlayerProvider"; import { usePlayer } from "../providers/PlayerProvider";
const Scene = () => { const Scene = () => {
const { isRegistered } = usePlayer(); const { isRegistered } = usePlayer();
const [isLeaderboardOpen, setIsLeaderboardOpen] = useState(false);
const handleMusicReady = useCallback((unmute: () => void) => { const handleMusicReady = useCallback((unmute: () => void) => {
if (isRegistered) { if (isRegistered) {
unmute(); unmute();
@ -25,6 +28,26 @@ const Scene = () => {
<div className={`${styles.bonfire} ${styles.background_asset}`} /> <div className={`${styles.bonfire} ${styles.background_asset}`} />
<Army /> <Army />
<MusicPlayer onReady={handleMusicReady} /> <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> </div>
} }

View File

@ -256,3 +256,69 @@
transform: scale(1.02, 1.03) skew(0deg, -1deg); 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);
}

View 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;
}

View 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[]
}