diff --git a/apps/web/src/app/products/such-moon-launch/leaderboard/page.tsx b/apps/web/src/app/products/such-moon-launch/leaderboard/page.tsx new file mode 100644 index 0000000..0cb7ae4 --- /dev/null +++ b/apps/web/src/app/products/such-moon-launch/leaderboard/page.tsx @@ -0,0 +1,255 @@ +import { Header } from "@/components/layout/header"; +import { MobileNav } from "@/components/layout/mobile-nav"; +import { Card } from "@repo/ui/components/card"; +import { JsonLd, breadcrumbLd } from "@/components/seo/json-ld"; +import { SuchMoonLaunchStoreButtons } from "@/components/store-buttons"; +import Link from "next/link"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Such Moon Launch — Global Leaderboard | Such Software", + description: + "Live global rankings for Such Moon Launch: fastest landings, biggest Moonrock hauls, and Endless Mode wave records across all 12 levels.", + alternates: { canonical: "/products/such-moon-launch/leaderboard" }, + openGraph: { + title: "Such Moon Launch — Global Leaderboard", + description: + "Live global rankings: fastest landings, biggest Moonrock hauls, and Endless Mode wave records.", + type: "website", + images: ["/images/og/such-moon-launch.png"], + }, + twitter: { card: "summary_large_image", images: ["/images/og/such-moon-launch.png"] }, +}; + +const API_BASE = + process.env.NEXT_PUBLIC_MOONLAUNCH_API ?? "https://api.such.software/v1/moonlaunch"; + +// index 0..11 maps to level 1..12 (level 12 = Endless Mode, wave board) +const LEVELS = [ + "Moon", "Mars", "Venus", "Io", "Jupiter", "Saturn", + "Neptune", "Pluto", "Asteroid Belt", "Space Station", "Mothership", "Endless", +]; + +type Score = { + nickname: string; + completion_time: number; + fuel_remaining: number; + crypto_collected: number; + stars: number; + wave: number; + platform: string | null; + rank: number; +}; + +// null = request failed (vs [] = no scores yet) +async function fetchBoard(level: number, board: string): Promise { + try { + const res = await fetch( + `${API_BASE}/scores?level=${level}&board=${board}&limit=50`, + { next: { revalidate: 60 } }, // cache the board for a minute + ); + if (!res.ok) return null; + const data = await res.json(); + return Array.isArray(data?.scores) ? data.scores : []; + } catch { + return null; + } +} + +function fmtTime(s: number): string { + if (s >= 60) { + const m = Math.floor(s / 60); + const sec = (s % 60).toFixed(1).padStart(4, "0"); + return `${m}:${sec}`; + } + return `${s.toFixed(1)}s`; +} + +function rankCell(rank: number): string { + return rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : String(rank); +} + +export default async function LeaderboardPage({ + searchParams, +}: { + searchParams: Promise<{ level?: string; board?: string }>; +}) { + const sp = await searchParams; + + let level = parseInt(sp.level ?? "1", 10); + if (!Number.isFinite(level) || level < 1 || level > 12) level = 1; + const isEndless = level === 12; + + let board = sp.board ?? (isEndless ? "wave" : "time"); + if (isEndless) board = "wave"; + else if (board !== "time" && board !== "score") board = "time"; + + const scores = await fetchBoard(level, board); + const levelName = level === 12 ? "Endless Mode" : LEVELS[level - 1]; + const metricLabel = board === "time" ? "Time" : board === "score" ? "Moonrocks" : "Wave"; + const base = "/products/such-moon-launch/leaderboard"; + + return ( +
+ +
+
+ + ← Such Moon Launch + + +

+ 🏆 Leaderboard +

+

+ Live global rankings — {levelName}. Updates every minute. +

+ + {/* Level selector */} +
+ {LEVELS.map((name, i) => { + const lv = i + 1; + const active = lv === level; + return ( + + {lv} + {name} + + ); + })} +
+ + {/* Board selector (story levels only) */} + {!isEndless && ( +
+ {([["time", "⏱ Fastest"], ["score", "💰 Most Moonrocks"]] as const).map(([key, label]) => { + const active = key === board; + return ( + + {label} + + ); + })} +
+ )} + + {/* Board */} + {scores === null ? ( + + Leaderboard is temporarily unavailable. Please try again shortly. + + ) : scores.length === 0 ? ( + + No scores yet for {levelName}. Be the first —{" "} + + play on web + + ! + + ) : ( + +
+ + + + + + + {board === "wave" ? ( + + ) : ( + + )} + + + + + {scores.map((s, i) => ( + + + + + {board === "wave" ? ( + + ) : ( + + )} + + + ))} + +
#Pilot{metricLabel}MoonrocksFuelStars
{rankCell(s.rank)}{s.nickname} + {board === "time" + ? fmtTime(s.completion_time) + : board === "score" + ? s.crypto_collected.toLocaleString() + : s.wave} + + {s.crypto_collected.toLocaleString()} + + {Math.round(s.fuel_remaining)}% + + {"★".repeat(Math.max(0, Math.min(3, s.stars)))} +
+
+
+ )} + +

+ Each pilot's best run per level.{" "} + {board === "time" + ? "Ranked by fastest completion time." + : board === "score" + ? "Ranked by most Moonrocks collected." + : "Ranked by highest wave survived."} +

+ +
+

Climb the ranks

+ +
+ +
+ + About the game + + • + + Play on web + + • + + Support + +
+
+ +
+ ); +} diff --git a/apps/web/src/app/products/such-moon-launch/page.tsx b/apps/web/src/app/products/such-moon-launch/page.tsx index 40c0347..802f85b 100644 --- a/apps/web/src/app/products/such-moon-launch/page.tsx +++ b/apps/web/src/app/products/such-moon-launch/page.tsx @@ -83,6 +83,12 @@ export default function SuchMoonLaunchPage() {
+ + 🏆 View the global leaderboard → +

The Story