diff --git a/app/components/Leaderboard.tsx b/app/components/Leaderboard.tsx index fb84b63..3b91c51 100644 --- a/app/components/Leaderboard.tsx +++ b/app/components/Leaderboard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useEffect } from 'react'; import { ENTRY_FEE_DINO, MAX_ATTEMPTS } from '../shared'; interface LeaderboardEntry { @@ -7,14 +8,88 @@ interface LeaderboardEntry { score: string; } +interface LeaderboardHistoryEntry { + owner: string; + highest_score: number; +} + +interface LeaderboardHistoryData { + leaderboard_id: number; + metadata: string; + total_players: number; + highest_score: number; + lowest_score: number; + average_score: number; + players: LeaderboardHistoryEntry[]; +} + +interface LeaderboardHistoryResponse { + success: boolean; + total_leaderboards: number; + data: LeaderboardHistoryData[]; +} + interface LeaderboardProps { leaderboard: LeaderboardEntry[]; loading: boolean; error: string | null; attemptsCount: number; + isRefreshing?: boolean; // New prop to track background refreshes + refreshCounter?: number; // New prop to track refresh cycles } -export default function Leaderboard({ leaderboard, loading, error, attemptsCount }: LeaderboardProps) { +export default function Leaderboard({ leaderboard, loading, error, attemptsCount, isRefreshing = false, refreshCounter = 0 }: LeaderboardProps) { + const [showHistory, setShowHistory] = useState(false); + const [historyData, setHistoryData] = useState([]); + const [historyLoading, setHistoryLoading] = useState(false); + const [historyError, setHistoryError] = useState(null); + const [currentLeaderboardIndex, setCurrentLeaderboardIndex] = useState(0); + + const fetchHistoryData = async () => { + try { + setHistoryLoading(true); + setHistoryError(null); + + const response = await fetch('https://vps.playpoolstudios.com/dino/api/get_leaderboard_history.php'); + + if (!response.ok) { + throw new Error('Failed to fetch leaderboard history'); + } + + const data: LeaderboardHistoryResponse = await response.json(); + + if (data.success && data.data.length > 0) { + setHistoryData(data.data); + setCurrentLeaderboardIndex(0); // Reset to first leaderboard + } else { + throw new Error('No history data available'); + } + } catch (err) { + console.error('Error fetching leaderboard history:', err); + setHistoryError(err instanceof Error ? err.message : 'Failed to fetch leaderboard history'); + } finally { + setHistoryLoading(false); + } + }; + + const openHistoryModal = () => { + setShowHistory(true); + fetchHistoryData(); + }; + + const nextLeaderboard = () => { + setCurrentLeaderboardIndex((prev) => + prev === historyData.length - 1 ? 0 : prev + 1 + ); + }; + + const prevLeaderboard = () => { + setCurrentLeaderboardIndex((prev) => + prev === 0 ? historyData.length - 1 : prev - 1 + ); + }; + + const currentLeaderboard = historyData[currentLeaderboardIndex]; // Only show loading state if we have no data yet if (loading && leaderboard.length === 0) { @@ -51,70 +126,93 @@ export default function Leaderboard({ leaderboard, loading, error, attemptsCount ); return ( -
-

🏆 Leaderboard

- - {/* Combined Prize and Attempts Information */} -
-
-

💰 Prize ({MAX_ATTEMPTS * ENTRY_FEE_DINO} DINO Total)

-
-
🎯 Attempts Remaining:
-
{MAX_ATTEMPTS - attemptsCount}
+ <> +
+
+

🏆 Leaderboard

+
+ {/* Update indicator */} + {isRefreshing && ( +
+
+ Updating...
-
-
-
- 🥇 1st Place: - {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.8).toFixed(1)} DINO (80%) -
-
- 🥈 2nd Place: - {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.1).toFixed(1)} DINO (10%) -
-
- 🥉 3rd Place: - {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.05).toFixed(1)} DINO (5%) -
-
-
-

Leaderboard resets after {MAX_ATTEMPTS} attempts

-
-
- - {sortedLeaderboard.length === 0 ? ( -
-

No scores yet. Be the first to play!

-
- ) : ( -
- {sortedLeaderboard.map((entry, index) => ( -
-
-
- {index + 1} -
-
-

- {entry.owner.length > 20 - ? `${entry.owner.slice(0, 8)}...${entry.owner.slice(-8)}` - : entry.owner - } -

- {/* Prize indicators for top 3 */} + + + + History + +
+
+ + {/* Combined Prize and Attempts Information */} +
+
+

💰 Prize ({MAX_ATTEMPTS * ENTRY_FEE_DINO} DINO Total)

+
+
🎯 Attempts Remaining:
+
{MAX_ATTEMPTS - attemptsCount}
+
+
+
+
+ 🥇 1st Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.8).toFixed(1)} DINO (80%) +
+
+ 🥈 2nd Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.1).toFixed(1)} DINO (10%) +
+
+ 🥉 3rd Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.05).toFixed(1)} DINO (5%) +
+
+
+

Leaderboard resets after {MAX_ATTEMPTS} attempts

+
+
+ + {sortedLeaderboard.length === 0 ? ( +
+

No scores yet. Be the first to play!

+
+ ) : ( +
+ {sortedLeaderboard.map((entry, index) => ( +
+
+
+ {index + 1} +
+
+

+ {entry.owner.length > 20 + ? `${entry.owner.slice(0, 8)}...${entry.owner.slice(-8)}` + : entry.owner + } +

+ {/* Prize indicators for top 3 */} {index < 3 && (
)} +
+
+
+

+ {entry.score} +

+

points

-
-

{entry.score}

-

points

-
+ ))} +
+ )} + +
+
+

+ {sortedLeaderboard.length} player{sortedLeaderboard.length !== 1 ? 's' : ''} +

+
+
+
+ + {/* History Modal */} + {showHistory && ( +
+
+ {/* Modal Header */} +
+

📊 Leaderboard History

+
- ))} + + {/* Modal Content */} +
+ {historyLoading ? ( +
+
+
+ ) : historyError ? ( +
+

{historyError}

+ +
+ ) : historyData.length > 0 ? ( +
+ {/* Leaderboard Carousel Navigation */} +
+ + +
+

+ Leaderboard #{currentLeaderboard.leaderboard_id} +

+

+ {currentLeaderboardIndex + 1} of {historyData.length} +

+
+ + +
+ + {/* Statistics Summary */} +
+
+
{currentLeaderboard.total_players}
+
Total Players
+
+
+
{currentLeaderboard.highest_score}
+
Highest Score
+
+
+
{currentLeaderboard.lowest_score}
+
Lowest Score
+
+
+
{currentLeaderboard.average_score.toFixed(1)}
+
Average Score
+
+
+ + {/* History Entries */} +
+

All Time Entries

+
+ {currentLeaderboard.players.map((entry, index) => ( +
+
+
+ {index + 1} +
+
+

+ {entry.owner.length > 20 + ? `${entry.owner.slice(0, 8)}...${entry.owner.slice(-8)}` + : entry.owner + } +

+
+
+
+

{entry.highest_score}

+

points

+
+
+ ))} +
+
+
+ ) : null} +
+ + {/* Modal Footer */} +
+ +
+
)} - -
-
-

- Updates every 5 seconds • {sortedLeaderboard.length} player{sortedLeaderboard.length !== 1 ? 's' : ''} -

- {loading && leaderboard.length > 0 && ( -
-
- Refreshing... -
- )} -
-
-
+ ); } diff --git a/app/page.tsx b/app/page.tsx index 047243e..a226ff0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -27,6 +27,8 @@ export default function Home() { const [dashboardData, setDashboardData] = useState({ leaderboard: [], available_tx: null, attempts_count: 0 }); const [dashboardLoading, setDashboardLoading] = useState(true); const [dashboardError, setDashboardError] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + const [refreshCounter, setRefreshCounter] = useState(0); const headerRef = useRef(null); // Poll for available tickets every 5 seconds (only when no ticket is available) @@ -101,9 +103,16 @@ export default function Home() { // Fetch dashboard data every 5 seconds useEffect(() => { - const fetchDashboardData = async () => { + const fetchDashboardData = async (isInitialFetch = false) => { try { - setDashboardLoading(true); + // Only show loading state on initial fetch, not on subsequent polls + if (isInitialFetch) { + setDashboardLoading(true); + } else { + // Show refreshing state for background updates + setIsRefreshing(true); + } + const response = await fetch('https://vps.playpoolstudios.com/dino/api/get_dashboard_data.php'); if (!response.ok) { @@ -113,18 +122,29 @@ export default function Home() { const data: DashboardData = await response.json(); setDashboardData(data); setDashboardError(null); + + // Increment refresh counter for smooth updates + if (!isInitialFetch) { + setRefreshCounter(prev => prev + 1); + } } catch (err) { console.error('Error fetching dashboard data:', err); setDashboardError(err instanceof Error ? err.message : 'Failed to fetch dashboard data'); } finally { - setDashboardLoading(false); + if (isInitialFetch) { + setDashboardLoading(false); + } else { + // Hide refreshing state after a short delay for smooth UX + setTimeout(() => setIsRefreshing(false), 500); + } } }; - fetchDashboardData(); + // Initial fetch with loading state + fetchDashboardData(true); - // Refresh dashboard data every 5 seconds - const interval = setInterval(fetchDashboardData, 5000); + // Refresh dashboard data every 5 seconds without loading state + const interval = setInterval(() => fetchDashboardData(false), 5000); return () => clearInterval(interval); }, []); @@ -374,6 +394,8 @@ export default function Home() { loading={dashboardLoading} error={dashboardError} attemptsCount={dashboardData.attempts_count} + isRefreshing={isRefreshing} + refreshCounter={refreshCounter} />