diff --git a/app/src/components/Leaderboard.tsx b/app/src/components/Leaderboard.tsx new file mode 100644 index 0000000..47f7287 --- /dev/null +++ b/app/src/components/Leaderboard.tsx @@ -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() + const [topRaiders, setTopRaiders] = useState() + 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 ( +
+

Leaderboard

+ +
+ + +
+ + {activeTab === 'earners' && ( +
+ {topEarners?.armies.map((army, index) => ( +
+ #{index + 1} + {army.player.id.slice(0, 6)}...{army.player.id.slice(-4)} + {formatUnits(BigInt(army.profitPerSecond), 4)} GELD/s +
+ ))} +
+ )} + + {activeTab === 'raiders' && ( +
+ {topRaiders?.players.map((player, index) => ( +
+ #{index + 1} + {player.id.slice(0, 6)}...{player.id.slice(-4)} + {player.numberOfRaids} raids +
+ ))} +
+ )} +
+ ) +} + +export default Leaderboard \ No newline at end of file diff --git a/app/src/components/Scene.tsx b/app/src/components/Scene.tsx index c80fd96..cbd9b9a 100644 --- a/app/src/components/Scene.tsx +++ b/app/src/components/Scene.tsx @@ -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 = () => {
+ + {isLeaderboardOpen && ( +
+
+ + +
+
+ )}
} diff --git a/app/src/styles/Background.module.css b/app/src/styles/Background.module.css index 83d7bb1..8bc6159 100644 --- a/app/src/styles/Background.module.css +++ b/app/src/styles/Background.module.css @@ -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); +} diff --git a/app/src/styles/Leaderboard.module.css b/app/src/styles/Leaderboard.module.css new file mode 100644 index 0000000..952f19f --- /dev/null +++ b/app/src/styles/Leaderboard.module.css @@ -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; + } \ No newline at end of file diff --git a/app/src/types/leaderboard.ts b/app/src/types/leaderboard.ts new file mode 100644 index 0000000..ef2507d --- /dev/null +++ b/app/src/types/leaderboard.ts @@ -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[] + } \ No newline at end of file