mirror of
https://github.com/Xevion/Pac-Man.git
synced 2026-01-31 08:25:06 -06:00
feat(web): add smooth page transitions and WASM loading states
- Implement navigation state tracking with optimistic UI updates - Add loading spinner and error handling for WASM initialization - Insert browser yield points during game initialization to prevent freezing - Redesign leaderboard with tabbed navigation and mock data structure - Add utility CSS classes for consistent page layouts
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { OnPageTransitionEndAsync } from "vike/types";
|
||||
import { getPacmanWindow } from "@/lib/pacman";
|
||||
import { setPendingNavigation } from "@/lib/navigation";
|
||||
|
||||
export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
||||
pageContext
|
||||
) => {
|
||||
export const onPageTransitionEnd: OnPageTransitionEndAsync = async (pageContext) => {
|
||||
console.log("Page transition end");
|
||||
setPendingNavigation(null);
|
||||
document.querySelector("body")?.classList.remove("page-is-transitioning");
|
||||
|
||||
// Restart the game loop when returning to the game page
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||
import { getPacmanWindow } from "@/lib/pacman";
|
||||
import { setPendingNavigation } from "@/lib/navigation";
|
||||
|
||||
// Must match --transition-duration in layouts/tailwind.css
|
||||
const TRANSITION_DURATION = 200;
|
||||
|
||||
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||
export const onPageTransitionStart: OnPageTransitionStartAsync = async (pageContext) => {
|
||||
console.log("Page transition start");
|
||||
setPendingNavigation(pageContext.urlPathname);
|
||||
document.querySelector("body")?.classList.add("page-is-transitioning");
|
||||
|
||||
// Stop the game loop when navigating away from the game page
|
||||
|
||||
@@ -2,18 +2,10 @@ import { usePageContext } from "vike-react/usePageContext";
|
||||
|
||||
export default function Page() {
|
||||
const { is404 } = usePageContext();
|
||||
if (is404) {
|
||||
return (
|
||||
<>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>This page could not be found.</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h1>Internal Error</h1>
|
||||
<p>Something went wrong.</p>
|
||||
</>
|
||||
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center px-4">
|
||||
<h1 className="text-4xl font-bold mb-4">{is404 ? "Page Not Found" : "Internal Error"}</h1>
|
||||
<p className="text-gray-400">{is404 ? "This page could not be found." : "Something went wrong."}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl py-8 px-4">
|
||||
<div className="page-container">
|
||||
<div className="space-y-6">
|
||||
<div className="border border-yellow-400/20 rounded-md bg-transparent p-6 shadow-[0_4px_20px_rgba(250,204,21,0.08)]">
|
||||
<div className="card">
|
||||
<h2 className="text-2xl font-bold mb-4">Download Pac-Man</h2>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Download instructions and releases will be available here soon.
|
||||
</p>
|
||||
<p className="text-gray-300 mb-4">Download instructions and releases will be available here soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import "../../layouts/tailwind.css";
|
||||
|
||||
export default function GameLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-black text-yellow-400 h-full flex flex-col overflow-hidden">
|
||||
|
||||
+100
-6
@@ -1,18 +1,50 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getPacmanWindow } from "@/lib/pacman";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getPacmanWindow, LoadingError } from "@/lib/pacman";
|
||||
|
||||
const LOADING_FADE_DURATION = 300;
|
||||
const LOADING_TIMEOUT_MS = 15000;
|
||||
|
||||
export default function Page() {
|
||||
const [gameReady, setGameReady] = useState(false);
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [loadingVisible, setLoadingVisible] = useState(true);
|
||||
const [loadError, setLoadError] = useState<LoadingError | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Fade out loading overlay when game becomes ready
|
||||
useEffect(() => {
|
||||
if (gameReady && loadingVisible) {
|
||||
const timer = setTimeout(() => {
|
||||
setLoadingVisible(false);
|
||||
}, LOADING_FADE_DURATION);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [gameReady, loadingVisible]);
|
||||
|
||||
// Clear timeout when game is ready or error occurs
|
||||
useEffect(() => {
|
||||
if (gameReady || loadError) {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [gameReady, loadError]);
|
||||
|
||||
useEffect(() => {
|
||||
const win = getPacmanWindow();
|
||||
|
||||
|
||||
// Always set up the ready callback (restart_game will call it too)
|
||||
win.pacmanReady = () => {
|
||||
setGameReady(true);
|
||||
};
|
||||
|
||||
// Error callback for WASM runtime errors
|
||||
win.pacmanError = (error: LoadingError) => {
|
||||
console.error("Pacman error:", error);
|
||||
setLoadError(error);
|
||||
};
|
||||
|
||||
const module = win.Module;
|
||||
|
||||
// If Module already exists (returning after navigation),
|
||||
@@ -27,6 +59,7 @@ export default function Page() {
|
||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
|
||||
if (!canvas) {
|
||||
console.error("Canvas element not found");
|
||||
setLoadError({ type: "runtime", message: "Canvas element not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,15 +69,36 @@ export default function Page() {
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
},
|
||||
preRun: [],
|
||||
// Emscripten calls this on fatal errors (abort/trap/etc)
|
||||
onAbort: (what: unknown) => {
|
||||
const message = typeof what === "string" ? what : "WebAssembly execution aborted";
|
||||
console.error("WASM abort:", what);
|
||||
setLoadError({ type: "runtime", message });
|
||||
},
|
||||
};
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "/pacman.js";
|
||||
script.async = false;
|
||||
|
||||
// Handle script load errors
|
||||
script.onerror = () => {
|
||||
setLoadError({ type: "script", message: "Failed to load game script" });
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Set up loading timeout - the separate effect clears this if game loads successfully
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setLoadError((prev) => prev ?? { type: "timeout" });
|
||||
}, LOADING_TIMEOUT_MS);
|
||||
|
||||
return () => {
|
||||
delete win.pacmanReady;
|
||||
delete win.pacmanError;
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -81,12 +135,52 @@ export default function Page() {
|
||||
>
|
||||
<canvas id="canvas" className="w-full h-full" />
|
||||
|
||||
{/* Loading overlay - CSS animation continues during main thread blocking */}
|
||||
{loadingVisible && (
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 transition-opacity"
|
||||
style={{
|
||||
transitionDuration: `${LOADING_FADE_DURATION}ms`,
|
||||
opacity: gameReady ? 0 : 1,
|
||||
}}
|
||||
>
|
||||
{loadError ? (
|
||||
<>
|
||||
<div className="error-indicator" />
|
||||
<span className="text-red-500 text-2xl mt-4 font-semibold">
|
||||
{loadError.type === "timeout"
|
||||
? "Loading timed out"
|
||||
: loadError.type === "script"
|
||||
? "Failed to load"
|
||||
: "Error occurred"}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mt-2 max-w-xs text-center">
|
||||
{loadError.type === "timeout"
|
||||
? "The game took too long to load. Please refresh the page."
|
||||
: loadError.type === "script"
|
||||
? "Could not load game files. Check your connection and refresh."
|
||||
: loadError.message}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 px-4 py-2 bg-yellow-400 text-black font-semibold rounded hover:bg-yellow-300 transition-colors"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="loading-spinner" />
|
||||
<span className="text-yellow-400 text-2xl mt-4">Loading...</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Click to Start overlay */}
|
||||
{gameReady && !gameStarted && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/60 cursor-pointer">
|
||||
<span className="text-yellow-400 text-5xl font-bold">
|
||||
Click to Start
|
||||
</span>
|
||||
<span className="text-yellow-400 text-5xl font-bold">Click to Start</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+12
-239
@@ -1,228 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { IconTrophy, IconCalendar } from "@tabler/icons-react";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
id: number;
|
||||
rank: number;
|
||||
name: string;
|
||||
score: number;
|
||||
duration: string;
|
||||
levelCount: number;
|
||||
submittedAt: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
const mockGlobalData: LeaderboardEntry[] = [
|
||||
{
|
||||
id: 1,
|
||||
rank: 1,
|
||||
name: "PacMaster2024",
|
||||
score: 125000,
|
||||
duration: "45:32",
|
||||
levelCount: 12,
|
||||
submittedAt: "2 hours ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PacMaster2024",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
rank: 2,
|
||||
name: "GhostHunter",
|
||||
score: 118750,
|
||||
duration: "42:18",
|
||||
levelCount: 11,
|
||||
submittedAt: "5 hours ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=GhostHunter",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rank: 3,
|
||||
name: "DotCollector",
|
||||
score: 112500,
|
||||
duration: "38:45",
|
||||
levelCount: 10,
|
||||
submittedAt: "1 day ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=DotCollector",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rank: 4,
|
||||
name: "MazeRunner",
|
||||
score: 108900,
|
||||
duration: "41:12",
|
||||
levelCount: 10,
|
||||
submittedAt: "2 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=MazeRunner",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rank: 5,
|
||||
name: "PowerPellet",
|
||||
score: 102300,
|
||||
duration: "36:28",
|
||||
levelCount: 9,
|
||||
submittedAt: "3 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PowerPellet",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
rank: 6,
|
||||
name: "CherryPicker",
|
||||
score: 98750,
|
||||
duration: "39:15",
|
||||
levelCount: 9,
|
||||
submittedAt: "4 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=CherryPicker",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
rank: 7,
|
||||
name: "BlinkyBeater",
|
||||
score: 94500,
|
||||
duration: "35:42",
|
||||
levelCount: 8,
|
||||
submittedAt: "5 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlinkyBeater",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
rank: 8,
|
||||
name: "PinkyPac",
|
||||
score: 91200,
|
||||
duration: "37:55",
|
||||
levelCount: 8,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PinkyPac",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
rank: 9,
|
||||
name: "InkyDestroyer",
|
||||
score: 88800,
|
||||
duration: "34:18",
|
||||
levelCount: 8,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=InkyDestroyer",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
rank: 10,
|
||||
name: "ClydeChaser",
|
||||
score: 85600,
|
||||
duration: "33:45",
|
||||
levelCount: 7,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ClydeChaser",
|
||||
},
|
||||
];
|
||||
|
||||
const mockMonthlyData: LeaderboardEntry[] = [
|
||||
{
|
||||
id: 1,
|
||||
rank: 1,
|
||||
name: "JanuaryChamp",
|
||||
score: 115000,
|
||||
duration: "43:22",
|
||||
levelCount: 11,
|
||||
submittedAt: "1 day ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=JanuaryChamp",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
rank: 2,
|
||||
name: "NewYearPac",
|
||||
score: 108500,
|
||||
duration: "40:15",
|
||||
levelCount: 10,
|
||||
submittedAt: "3 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=NewYearPac",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rank: 3,
|
||||
name: "WinterWarrior",
|
||||
score: 102000,
|
||||
duration: "38:30",
|
||||
levelCount: 10,
|
||||
submittedAt: "5 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=WinterWarrior",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rank: 4,
|
||||
name: "FrostyPac",
|
||||
score: 98500,
|
||||
duration: "37:45",
|
||||
levelCount: 9,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrostyPac",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rank: 5,
|
||||
name: "IceBreaker",
|
||||
score: 95200,
|
||||
duration: "36:12",
|
||||
levelCount: 9,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=IceBreaker",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
rank: 6,
|
||||
name: "SnowPac",
|
||||
score: 91800,
|
||||
duration: "35:28",
|
||||
levelCount: 8,
|
||||
submittedAt: "2 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=SnowPac",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
rank: 7,
|
||||
name: "BlizzardBeast",
|
||||
score: 88500,
|
||||
duration: "34:15",
|
||||
levelCount: 8,
|
||||
submittedAt: "2 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlizzardBeast",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
rank: 8,
|
||||
name: "ColdSnap",
|
||||
score: 85200,
|
||||
duration: "33:42",
|
||||
levelCount: 8,
|
||||
submittedAt: "3 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ColdSnap",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
rank: 9,
|
||||
name: "FrozenFury",
|
||||
score: 81900,
|
||||
duration: "32:55",
|
||||
levelCount: 7,
|
||||
submittedAt: "3 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrozenFury",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
rank: 10,
|
||||
name: "ArcticAce",
|
||||
score: 78600,
|
||||
duration: "31:18",
|
||||
levelCount: 7,
|
||||
submittedAt: "4 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ArcticAce",
|
||||
},
|
||||
];
|
||||
import { mockGlobalData, mockMonthlyData, type LeaderboardEntry } from "./mockData";
|
||||
|
||||
function LeaderboardTable({ data }: { data: LeaderboardEntry[] }) {
|
||||
return (
|
||||
<table className="w-full border-separate border-spacing-y-2">
|
||||
<tbody>
|
||||
{data.map((entry, entryIndex) => (
|
||||
{data.map((entry) => (
|
||||
<tr key={entry.id} className="bg-black">
|
||||
<td className="py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -234,9 +18,7 @@ function LeaderboardTable({ data }: { data: LeaderboardEntry[] }) {
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<span className="text-yellow-300 font-[600] text-lg">
|
||||
{entry.score.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-yellow-300 font-[600] text-lg">{entry.score.toLocaleString()}</span>
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<span className="text-gray-300">{entry.duration}</span>
|
||||
@@ -249,33 +31,24 @@ function LeaderboardTable({ data }: { data: LeaderboardEntry[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
const tabButtonClass = (isActive: boolean) =>
|
||||
`inline-flex items-center gap-1 px-3 py-1 rounded border ${
|
||||
isActive ? "border-yellow-400/40 text-yellow-300" : "border-transparent text-gray-300 hover:text-yellow-200"
|
||||
}`;
|
||||
|
||||
export default function Page() {
|
||||
const [activeTab, setActiveTab] = useState<"global" | "monthly">("global");
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl py-8 px-4">
|
||||
<div className="page-container">
|
||||
<div className="space-y-6">
|
||||
<div className="border border-yellow-400/20 rounded-md bg-transparent p-6 shadow-[0_4px_20px_rgba(250,204,21,0.08)]">
|
||||
<div className="card">
|
||||
<div className="flex gap-2 border-b border-yellow-400/20 pb-2 mb-4">
|
||||
<button
|
||||
onClick={() => setActiveTab("global")}
|
||||
className={
|
||||
activeTab === "global"
|
||||
? "inline-flex items-center gap-1 px-3 py-1 rounded border border-yellow-400/40 text-yellow-300"
|
||||
: "inline-flex items-center gap-1 px-3 py-1 rounded border border-transparent text-gray-300 hover:text-yellow-200"
|
||||
}
|
||||
>
|
||||
<button onClick={() => setActiveTab("global")} className={tabButtonClass(activeTab === "global")}>
|
||||
<IconTrophy size={16} />
|
||||
Global
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("monthly")}
|
||||
className={
|
||||
activeTab === "monthly"
|
||||
? "inline-flex items-center gap-1 px-3 py-1 rounded border border-yellow-400/40 text-yellow-300"
|
||||
: "inline-flex items-center gap-1 px-3 py-1 rounded border border-transparent text-gray-300 hover:text-yellow-200"
|
||||
}
|
||||
>
|
||||
<button onClick={() => setActiveTab("monthly")} className={tabButtonClass(activeTab === "monthly")}>
|
||||
<IconCalendar size={16} />
|
||||
Monthly
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Config } from "vike/types";
|
||||
|
||||
export default {
|
||||
prerender: true, // Generate static HTML for deployment
|
||||
ssr: false, // Force client-side only rendering
|
||||
prerender: true, // Generate static HTML for deployment
|
||||
ssr: false, // Force client-side only rendering
|
||||
} satisfies Config;
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
export interface LeaderboardEntry {
|
||||
id: number;
|
||||
rank: number;
|
||||
name: string;
|
||||
score: number;
|
||||
duration: string;
|
||||
levelCount: number;
|
||||
submittedAt: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export const mockGlobalData: LeaderboardEntry[] = [
|
||||
{
|
||||
id: 1,
|
||||
rank: 1,
|
||||
name: "PacMaster2024",
|
||||
score: 125000,
|
||||
duration: "45:32",
|
||||
levelCount: 12,
|
||||
submittedAt: "2 hours ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PacMaster2024",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
rank: 2,
|
||||
name: "GhostHunter",
|
||||
score: 118750,
|
||||
duration: "42:18",
|
||||
levelCount: 11,
|
||||
submittedAt: "5 hours ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=GhostHunter",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rank: 3,
|
||||
name: "DotCollector",
|
||||
score: 112500,
|
||||
duration: "38:45",
|
||||
levelCount: 10,
|
||||
submittedAt: "1 day ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=DotCollector",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rank: 4,
|
||||
name: "MazeRunner",
|
||||
score: 108900,
|
||||
duration: "41:12",
|
||||
levelCount: 10,
|
||||
submittedAt: "2 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=MazeRunner",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rank: 5,
|
||||
name: "PowerPellet",
|
||||
score: 102300,
|
||||
duration: "36:28",
|
||||
levelCount: 9,
|
||||
submittedAt: "3 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PowerPellet",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
rank: 6,
|
||||
name: "CherryPicker",
|
||||
score: 98750,
|
||||
duration: "39:15",
|
||||
levelCount: 9,
|
||||
submittedAt: "4 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=CherryPicker",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
rank: 7,
|
||||
name: "BlinkyBeater",
|
||||
score: 94500,
|
||||
duration: "35:42",
|
||||
levelCount: 8,
|
||||
submittedAt: "5 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlinkyBeater",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
rank: 8,
|
||||
name: "PinkyPac",
|
||||
score: 91200,
|
||||
duration: "37:55",
|
||||
levelCount: 8,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PinkyPac",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
rank: 9,
|
||||
name: "InkyDestroyer",
|
||||
score: 88800,
|
||||
duration: "34:18",
|
||||
levelCount: 8,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=InkyDestroyer",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
rank: 10,
|
||||
name: "ClydeChaser",
|
||||
score: 85600,
|
||||
duration: "33:45",
|
||||
levelCount: 7,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ClydeChaser",
|
||||
},
|
||||
];
|
||||
|
||||
export const mockMonthlyData: LeaderboardEntry[] = [
|
||||
{
|
||||
id: 1,
|
||||
rank: 1,
|
||||
name: "JanuaryChamp",
|
||||
score: 115000,
|
||||
duration: "43:22",
|
||||
levelCount: 11,
|
||||
submittedAt: "1 day ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=JanuaryChamp",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
rank: 2,
|
||||
name: "NewYearPac",
|
||||
score: 108500,
|
||||
duration: "40:15",
|
||||
levelCount: 10,
|
||||
submittedAt: "3 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=NewYearPac",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rank: 3,
|
||||
name: "WinterWarrior",
|
||||
score: 102000,
|
||||
duration: "38:30",
|
||||
levelCount: 10,
|
||||
submittedAt: "5 days ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=WinterWarrior",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rank: 4,
|
||||
name: "FrostyPac",
|
||||
score: 98500,
|
||||
duration: "37:45",
|
||||
levelCount: 9,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrostyPac",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rank: 5,
|
||||
name: "IceBreaker",
|
||||
score: 95200,
|
||||
duration: "36:12",
|
||||
levelCount: 9,
|
||||
submittedAt: "1 week ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=IceBreaker",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
rank: 6,
|
||||
name: "SnowPac",
|
||||
score: 91800,
|
||||
duration: "35:28",
|
||||
levelCount: 8,
|
||||
submittedAt: "2 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=SnowPac",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
rank: 7,
|
||||
name: "BlizzardBeast",
|
||||
score: 88500,
|
||||
duration: "34:15",
|
||||
levelCount: 8,
|
||||
submittedAt: "2 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlizzardBeast",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
rank: 8,
|
||||
name: "ColdSnap",
|
||||
score: 85200,
|
||||
duration: "33:42",
|
||||
levelCount: 8,
|
||||
submittedAt: "3 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ColdSnap",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
rank: 9,
|
||||
name: "FrozenFury",
|
||||
score: 81900,
|
||||
duration: "32:55",
|
||||
levelCount: 7,
|
||||
submittedAt: "3 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrozenFury",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
rank: 10,
|
||||
name: "ArcticAce",
|
||||
score: 78600,
|
||||
duration: "31:18",
|
||||
levelCount: 7,
|
||||
submittedAt: "4 weeks ago",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ArcticAce",
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user