From 949aa6627aa612af1481759a9532ce6d529b7fb0 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Tue, 6 Oct 2026 15:21:49 +0500 Subject: [PATCH] Add player leaderboard and homepage preview --- README.md | 13 + app/home-map.css | 69 ++ app/layout.tsx | 3 + app/leaderboard.css | 1080 +++++++++++++++++ app/leaderboard/loading.tsx | 24 + app/leaderboard/page.tsx | 217 ++++ app/page.tsx | 29 +- app/rounded-theme.css | 272 +++++ components/Leaderboard.tsx | 203 ++++ components/MapCard.tsx | 30 +- components/PixelArt.tsx | 56 + components/Results.tsx | 3 + components/Shell.tsx | 8 +- components/SiteNav.tsx | 31 + lib/leaderboard.ts | 36 + next-env.d.ts | 4 +- package.json | 1 + public/cursors/orbit-link.svg | 5 + public/cursors/orbit.svg | 5 + public/images/minecraft-map-background.png | Bin 0 -> 304 bytes scripts/test-leaderboard-sql.mjs | 137 +++ shared/database.ts | 9 + shared/leaderboard.ts | 54 + .../20261006093345_player_leaderboard.sql | 61 + tests/leaderboard.test.ts | 41 + 25 files changed, 2363 insertions(+), 28 deletions(-) create mode 100644 app/home-map.css create mode 100644 app/leaderboard.css create mode 100644 app/leaderboard/loading.tsx create mode 100644 app/leaderboard/page.tsx create mode 100644 app/rounded-theme.css create mode 100644 components/Leaderboard.tsx create mode 100644 components/PixelArt.tsx create mode 100644 components/SiteNav.tsx create mode 100644 lib/leaderboard.ts create mode 100644 public/cursors/orbit-link.svg create mode 100644 public/cursors/orbit.svg create mode 100644 public/images/minecraft-map-background.png create mode 100644 scripts/test-leaderboard-sql.mjs create mode 100644 shared/leaderboard.ts create mode 100644 supabase/migrations/20261006093345_player_leaderboard.sql create mode 100644 tests/leaderboard.test.ts diff --git a/README.md b/README.md index 4581576..1eaa61e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,17 @@ The build copies both JARs to their Git-visible download folders. Include those ## Checks +The public `/leaderboard` and homepage preview rank signed-in players by their +best completed five-round expedition. Ties use average score, then games played. +Difficulty filters, literal player-name search, and pagination run in Postgres; +guest and incomplete games are excluded. Apply the `player_leaderboard` migration +when updating an existing installation. The score query is server-only and does +not expose session tokens, round locations, or contact information. + +`npm run test:leaderboard` verifies ranking and permissions in an isolated +Postgres runtime. It uses the optional PGlite package already used for SQL checks +(`npm install --prefix .data/sql-verification --no-save --package-lock=false @electric-sql/pglite@0.3.14`). + Accounts support display names, email/password login and password recovery. See [account settings and Supabase configuration](docs/account-settings.md). ```sh @@ -77,3 +88,5 @@ The renderer regression exercises the product's headless terrain worker with gra Plugin exports include generated Overworld surface terrain (up to 24 blocks below each column's top), vanilla block states, and biome identifiers. Buried opaque blocks are omitted. Each export is limited to 5 million retained blocks, 240 MiB of raw terrain JSON, and 50 MiB compressed. Start with a 128 × 128 area, check the preview, then increase coverage. Lighting is simplified; custom packs, entities, and dynamic block-entity visuals are not captured by the server exporter. The pixel heading font is VT323 by Peter Hull, distributed under the SIL Open Font License. Its license is in `public/fonts/OFL-VT323.txt`. + +The homepage uses Minecraft 1.21.11’s original `assets/minecraft/textures/map/map_background.png`, extracted unchanged from the cached vanilla client to `public/images/minecraft-map-background.png`. The texture belongs to Mojang; its pixel detail and transparent parchment edges are retained. diff --git a/app/home-map.css b/app/home-map.css new file mode 100644 index 0000000..317d604 --- /dev/null +++ b/app/home-map.css @@ -0,0 +1,69 @@ +/* Vanilla 1.21.11 map_background.png, rendered with its original alpha edges. */ +.card-map-content { + position: absolute; + inset: 0; +} +.home-page .minecraft-paper { + --map-paper-inset: 9.375%; + border: 0; + outline: 0; + clip-path: none; + background: url("/images/minecraft-map-background.png") center / 100% 100% + no-repeat; + box-shadow: none; + image-rendering: pixelated; + filter: drop-shadow(4px 6px 0 #04080c99); +} +.home-page .map-art-frame { + width: 320px; + height: 320px; + transform: none; + padding: 0; +} +.home-page .map-art-frame img { + position: absolute; + inset: var(--map-paper-inset); + width: 81.25%; + height: 81.25%; + display: block; + object-fit: contain; + image-rendering: pixelated; +} +.home-page .card-image .card-map-content { + inset: var(--map-paper-inset); +} +.home-page .minecraft-paper.card-image .card-map-content img { + object-fit: contain; +} +.home-page .map-card:hover .minecraft-paper img { + transform: none; +} +.home-page .minecraft-paper .card-type { + top: -16px; + left: 0; +} +.home-page .empty-map-art { + aspect-ratio: 1; + padding: 0; + transform: none; +} +.home-page .empty-map-art img { + position: absolute; + inset: var(--map-paper-inset); + width: 81.25%; + height: 81.25%; + object-fit: contain; + filter: none; +} +@media (max-width: 1000px) { + .home-page .map-art-frame { + width: 288px; + height: 288px; + } +} +@media (max-width: 700px) { + .home-page .map-art-frame { + width: 256px; + height: 256px; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index b15d324..1e8b4a2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,9 @@ import "./block-theme.css"; import "./account.css"; import "./map-theme.css"; import "./workshop.css"; +import "./leaderboard.css"; +import "./home-map.css"; +import "./rounded-theme.css"; export const metadata: Metadata = { title: { default: "MineGeo — Get lost in a familiar world", diff --git a/app/leaderboard.css b/app/leaderboard.css new file mode 100644 index 0000000..3178f09 --- /dev/null +++ b/app/leaderboard.css @@ -0,0 +1,1080 @@ +/* Quiet stone surfaces; pixel type and hard edges carry the Minecraft character. */ +:root { + --board-stone: #151e24; + --board-edge: #354149; + --board-gold: #d6b665; +} +body { + font-variant-numeric: tabular-nums; +} +h1, +h2, +h3 { + text-wrap: balance; +} +p { + text-wrap: pretty; +} +a, +button { + transition: + color 160ms, + background-color 160ms, + border-color 160ms, + transform 160ms; +} +a:focus-visible, +button:focus-visible, +input:focus-visible { + outline: 2px solid var(--green); + outline-offset: 5px; +} +.site-header { + background: #0c1219; +} +.site-header nav a { + position: relative; + white-space: nowrap; +} +.site-header nav a[aria-current="page"] { + color: var(--green); +} +.site-header nav a[aria-current="page"]::after { + content: ""; + position: absolute; + bottom: -10px; + left: 0; + right: 0; + height: 2px; + background: var(--green); +} +.header-account { + border-color: #526048; + border-bottom-color: #1d2917; + background: #23301f; + color: #c7d6bb; +} +.header-account:hover { + background: #31422a; +} +.home-page { + background-image: none; +} +.hero { + padding-top: 64px; + padding-bottom: 62px; + gap: 32px; + grid-template-columns: 1.15fr 1fr; +} +.hero h1 { + font-size: clamp(52px, 5.6vw, 76px); + line-height: 1; + margin-bottom: 24px; +} +.hero p { + max-width: 470px; + font-size: 14px; + line-height: 1.85; + color: #a7b1b7; +} +.status-label { + padding: 0; + border: 0; + background: none; + color: #96aa88; + letter-spacing: 1.6px; + font-size: 10px; + margin-bottom: 23px; +} +.hero-actions { + gap: 24px; + flex-wrap: wrap; +} +.hero-actions .text-link { + font-size: 11px; +} +.hero-notes { + margin-top: 27px; + gap: 12px; +} +.round-blocks { + display: flex; + gap: 4px !important; +} +.round-blocks i { + width: 8px; + height: 8px; + background: #749752; + border-top: 2px solid #99bc73; +} +.hero-art { + min-height: 346px; +} +.map-art-frame { + width: 310px; + height: 310px; + transform: rotate(-4deg); +} +.compass-art { + right: 5%; + top: 12px; + width: 58px; + height: 62px; + background: #1b2630; +} +.compass-art small { + left: 23px; +} +.floating-tag { + bottom: 1px; + background: #101923; + border-width: 1px; + padding: 9px 13px; +} +.grass-divider { + height: 15px; + border-top-width: 4px; + opacity: 0.85; +} +.grass-divider span { + height: 4px; +} +.world-stats { + margin-top: 0; + padding: 25px 0; + background: none; + border: 0; + border-bottom: 1px solid #2a353c; + box-shadow: none; +} +.world-stats > div { + padding-left: 20px; + gap: 12px; +} +.world-stats > div:first-child { + padding-left: 0; +} +.world-stats .stat-cube { + width: 16px; + height: 16px; + border-width: 1px; + box-shadow: + inset 2px 2px #ffffff20, + 2px 2px #03070b; +} +.world-stats .stat-cube:not(.green-cube) { + background: #58656e; + border-color: #8a969c; +} +.world-stats strong { + font-size: 33px; +} +.world-stats span:not(.live-dot) { + font-size: 9px; + letter-spacing: 0.8px; + margin-top: 5px; +} +.home-page .catalog-section { + padding-top: 52px; + padding-bottom: 52px; +} +.home-page .map-card { + box-shadow: 0 3px #03070b; + border-width: 1px; +} +.home-page .card-body { + background: #141e28; + padding: 16px 13px; +} +.home-page .card-body .eyebrow { + font-size: 9px; +} +.home-page .card-body p { + font-size: 12px; + min-height: 36px; +} +.home-page .card-meta { + font-size: 10px; +} +.home-panels { + gap: 40px; + padding-top: 56px; +} +.home-panel { + background: none; + box-shadow: none; + border: 0; + padding: 0; +} +.home-panel + .home-panel { + padding-left: 36px; + border-left: 1px solid #2b3841; +} +.play-steps { + gap: 24px; +} +.play-steps li > span { + background: none; + border: 0; + padding: 0; + color: #8a9c7c; +} +.mode-row { + border-width: 1px; + box-shadow: inset 0 1px #ffffff06; + background: #151f29; +} +.contribute-banner { + background: #151f23; + border-width: 1px; + border-left: 4px solid #73984e; +} +.pixel-trophy { + width: 30px; + height: 30px; + flex-shrink: 0; +} +.player-avatar { + width: 36px; + height: 36px; + flex-shrink: 0; + background: #314034; + box-shadow: 2px 2px #03070b; +} +.home-leaderboard { + display: grid; + grid-template-columns: 0.85fr 1.5fr; + gap: 64px; + align-items: center; + padding-top: 38px; + padding-bottom: 48px; + border-top: 1px solid #29343d; +} +.leaderboard-kicker { + display: flex; + gap: 11px; + align-items: center; + margin-bottom: 20px; +} +.leaderboard-kicker .eyebrow { + margin: 0; + color: #c1af7f; +} +.leaderboard-preview-copy h2 { + font-size: 54px; + line-height: 0.98; + margin-bottom: 19px; +} +.leaderboard-preview-copy h2 span { + color: var(--green); +} +.leaderboard-preview-copy p { + color: #9ba8ad; + font-size: 13px; + line-height: 1.9; + margin-bottom: 26px; +} +.leaderboard-preview-copy .text-link { + color: #c8d8bd; +} +.leaderboard-preview-board, +.rankings-board { + background: var(--board-stone); + border: 1px solid var(--board-edge); + border-top: 3px solid #566253; + box-shadow: 4px 4px #04080c; +} +.board-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 18px 22px; +} +.board-heading > span:first-child { + font: + 26px/1 MineGeoPixel, + monospace; +} +.board-heading > span:last-child { + color: #9ba8ad; + font-size: 10px; +} +.board-footnote { + padding: 12px 22px; + border-top: 1px solid #2e3a42; + font-size: 10px; + color: #92a092; +} +.leaderboard-table-wrap { + overflow-x: auto; +} +.leaderboard-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + text-align: left; +} +.leaderboard-table th, +.leaderboard-table td { + padding: 17px 22px; + border-top: 1px solid #2a353d; +} +.leaderboard-table thead { + background: #101920; +} +.leaderboard-table thead th { + color: #99a6ac; + font-size: 10px; + letter-spacing: 0.8px; + font-weight: 500; + text-transform: uppercase; + padding-top: 12px; + padding-bottom: 12px; +} +.leaderboard-table tbody th { + font-weight: 500; +} +.leaderboard-table tbody tr:hover { + background: #1a262c; +} +.leaderboard-table .rank-column { + width: 78px; + padding-right: 8px; +} +.rank-number { + font: + 24px/1 MineGeoPixel, + monospace; + color: #88969d; +} +.rank-number.place-1 { + color: var(--board-gold); +} +.rank-number.place-2 { + color: #bec8cf; +} +.rank-number.place-3 { + color: #b69a80; +} +.leaderboard-player { + display: flex; + align-items: center; + gap: 14px; + min-width: 0; +} +.player-name { + overflow-wrap: anywhere; + min-width: 0; +} +.leaderboard-table .score-column { + text-align: right; + white-space: nowrap; +} +.score-column strong { + font: + 25px/1 MineGeoPixel, + monospace; + color: #dce9d1; +} +.score-unit { + color: #8a9b89; + font-size: 10px; +} +.leaderboard-table .games-column, +.leaderboard-table .average-column { + text-align: right; + color: #aeb9bf; + white-space: nowrap; +} +.compact .leaderboard-table th, +.compact .leaderboard-table td { + padding: 13px 22px; +} +.compact .leaderboard-table .rank-column { + width: 58px; + padding-right: 0; +} +.compact .leaderboard-player { + gap: 12px; +} +.compact .player-avatar { + width: 30px; + height: 30px; +} +.compact .player-name { + font-size: 12px; +} +.your-row { + background: #213026; +} +.you-label { + margin-left: 9px; + font-size: 9px; + color: var(--green); + border: 1px solid #4f6841; + padding: 2px 4px; +} +.leaderboard-page { + padding-top: 32px; +} +.leaderboard-back { + font-size: 11px; + margin-bottom: 36px; +} +.leaderboard-intro { + display: flex; + justify-content: space-between; + align-items: center; + gap: 32px; +} +.leaderboard-intro h1 { + font-size: clamp(48px, 6.5vw, 76px); + margin: 12px 0 17px; +} +.leaderboard-intro .lead { + font-size: 14px; + line-height: 1.8; + margin-bottom: 0; +} +.intro-trophy { + width: 106px; + height: 106px; + margin-right: 32px; + filter: drop-shadow(6px 6px #04080c); +} +.leaderboard-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin: 38px 0 0; + padding-bottom: 14px; + border-bottom: 1px solid #354149; +} +.leaderboard-filters { + gap: 5px; + font-size: 12px; + flex-wrap: wrap; +} +.leaderboard-filters a { + padding: 8px 14px; + border: 1px solid transparent; +} +.leaderboard-filters a:hover { + color: #d9e8d0; + background: #18242c; +} +.leaderboard-filters a[aria-current="page"] { + color: #d7e8ca; + background: #243321; + border-color: #4c6340; + box-shadow: inset 0 2px #819c5a30; +} +.leaderboard-period { + color: #a4afa9; + font: 11px monospace; + white-space: nowrap; +} +.winners-podium { + display: grid; + grid-template-columns: 1fr 1.12fr 1fr; + align-items: end; + gap: 12px; + max-width: 850px; + margin: 42px auto 38px; +} +.podium-place { + text-align: center; + min-width: 0; + grid-row: 1; +} +.podium-1 { + grid-column: 2; +} +.podium-2 { + grid-column: 1; +} +.podium-3 { + grid-column: 3; +} +.podium-player { + padding: 0 12px 21px; +} +.podium-label { + display: block; + font: 9px/1.5 monospace; + color: #a7b4bc; + letter-spacing: 1.5px; + margin-bottom: 14px; +} +.podium-1 .podium-label { + color: var(--board-gold); +} +.podium-player .player-avatar { + width: 52px; + height: 52px; + margin: 0 auto 13px; +} +.podium-1 .player-avatar { + width: 64px; + height: 64px; + outline: 2px solid #9a814c; + outline-offset: 4px; + margin-bottom: 17px; +} +.podium-player h2 { + font-size: 28px; + overflow-wrap: anywhere; + line-height: 1; + margin-bottom: 8px; +} +.podium-player p { + font: + 26px/1.2 MineGeoPixel, + monospace; + margin: 0; + color: #d8e4ce; +} +.podium-player p span { + font: + 10px Arial, + sans-serif; + color: #839293; + white-space: nowrap; +} +.podium-block { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + height: 84px; + border: 1px solid #46545b; + border-top: 5px solid #66757b; + background: + repeating-linear-gradient(0deg, transparent 0 23px, #050a0d16 23px 24px), + #202d34; + box-shadow: + inset 5px 0 #ffffff04, + inset -5px 0 #050a0d40, + 0 4px #04080c; +} +.podium-block > span { + font: + 44px/1 MineGeoPixel, + monospace; + color: #94a3ad; + text-shadow: 2px 2px #101920; +} +.podium-1 .podium-block { + height: 112px; + border-color: #66734b; + border-top-color: #9caf73; + background-color: #2a3828; +} +.podium-1 .podium-block > span { + color: #c9d8aa; +} +.podium-1 .podium-block .pixel-trophy { + width: 38px; + height: 38px; +} +.podium-3 .podium-block { + height: 64px; + border-top-color: #7e7163; + background-color: #2d2d29; + border-color: #514c43; +} +.rankings-board { + margin-top: 32px; +} +.rankings-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 22px; +} +.rankings-heading h2 { + font-size: 32px; + line-height: 1; + margin-bottom: 7px; +} +.rankings-heading p { + color: #99a6ad; + font-size: 11px; + margin: 0; +} +.leaderboard-search { + display: flex; + align-items: center; + max-width: 280px; + background: #0d161d; + border: 1px solid #3b4851; +} +.leaderboard-search input { + width: 100%; + min-width: 0; + border: 0; + background: none; + color: var(--text); + padding: 10px 12px; + font-size: 12px; +} +.leaderboard-search input::placeholder { + color: #87949b; +} +.leaderboard-search button { + padding: 10px 12px; + background: #202c34; + color: #b7c6ac; + border: 0; + border-left: 1px solid #3b4851; +} +.leaderboard-search button:hover { + background: #32452e; +} +.leaderboard-search button:active { + transform: translateY(1px); +} +.search-summary { + padding: 12px 22px; + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 12px; + border-top: 1px solid #354149; +} +.search-summary > span { + overflow-wrap: anywhere; +} +.search-summary a { + color: var(--green); + flex-shrink: 0; +} +.leaderboard-pagination { + padding: 18px 22px; + border-top: 1px solid #354149; + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + font-size: 11px; + color: #9faeb5; +} +.leaderboard-pagination nav { + display: flex; + align-items: center; + gap: 20px; + font-size: 11px; +} +.leaderboard-pagination nav a, +.leaderboard-pagination nav > span { + display: inline-flex; + align-items: center; + gap: 7px; +} +.leaderboard-pagination nav [aria-disabled="true"] { + color: #7d8b94; +} +.page-number { + color: #d3decb; + font-family: monospace; + white-space: nowrap; +} +.leaderboard-rules { + display: grid; + grid-template-columns: 0.9fr 1.2fr; + gap: 60px; + border-top: 1px solid #2d3942; + margin-top: 44px; + padding-top: 30px; +} +.leaderboard-rules h2 { + font-size: 33px; + max-width: 310px; +} +.leaderboard-rules p { + font-size: 12px; + color: #a5b0b5; + line-height: 1.85; + margin-bottom: 12px; +} +.leaderboard-rules .text-link { + color: #c4d6b8; + margin-top: 5px; +} +.leaderboard-empty { + text-align: center; + padding: 48px 24px; +} +.leaderboard-empty > .pixel-trophy { + width: 45px; + height: 45px; + margin: 0 auto 18px; + opacity: 0.85; +} +.leaderboard-empty h2 { + font-size: 32px; + margin-bottom: 12px; +} +.leaderboard-empty p { + font-size: 12px; + max-width: 400px; + margin: 0 auto 24px; + color: #a5b0b5; + line-height: 1.8; +} +.leaderboard-preview-board .leaderboard-empty { + padding: 27px 18px; +} +.leaderboard-preview-board .leaderboard-empty > .pixel-trophy { + display: none; +} +.leaderboard-preview-board .leaderboard-empty h2 { + font-size: 28px; +} +.leaderboard-preview-board .leaderboard-empty .button { + font-size: 20px; +} +.leaderboard-skeleton { + border: 1px solid #354149; + background: var(--board-stone); + margin-top: 40px; +} +.leaderboard-skeleton > div { + padding: 24px; + display: grid; + grid-template-columns: 36px 1fr 110px; + gap: 24px; + border-bottom: 1px solid #354149; +} +.leaderboard-skeleton span { + height: 24px; + background: #29373e; + animation: board-pulse 1.5s ease-in-out infinite alternate; +} +@keyframes board-pulse { + to { + opacity: 0.4; + } +} +@media (max-width: 1000px) { + .site-header nav { + gap: 19px; + font-size: 21px; + } + .header-inner { + gap: 20px; + } + .hero { + padding-top: 48px; + padding-bottom: 48px; + gap: 18px; + } + .hero h1 { + font-size: 59px; + } + .map-art-frame { + width: 280px; + height: 280px; + } + .hero-art { + min-height: 318px; + } + .home-leaderboard { + gap: 30px; + grid-template-columns: 0.8fr 1.4fr; + } + .leaderboard-preview-copy h2 { + font-size: 46px; + } + .leaderboard-table th, + .leaderboard-table td { + padding-left: 16px; + padding-right: 16px; + } +} +@media (max-width: 700px) { + .header-inner nav { + font-size: 20px; + gap: 8px; + } + .site-header nav a[aria-current="page"]::after { + bottom: -6px; + } + .hero { + padding: 36px 22px 32px; + } + .hero h1 { + font-size: clamp(46px, 9vw, 62px); + margin-bottom: 20px; + } + .hero p { + font-size: 13px; + } + .hero-actions { + gap: 16px; + } + .hero-art { + min-height: 290px; + margin-top: 20px; + } + .map-art-frame { + width: 254px; + height: 254px; + } + .compass-art { + right: calc(50% - 144px); + top: 0; + } + .world-stats { + padding: 24px 0; + gap: 23px 16px; + } + .world-stats > div { + padding-left: 0; + } + .world-stats strong { + font-size: 30px; + } + .world-stats span:not(.live-dot) { + font-size: 8px; + letter-spacing: 0.4px; + } + .home-page .catalog-section { + padding-top: 34px; + padding-bottom: 34px; + } + .home-leaderboard { + grid-template-columns: 1fr; + gap: 25px; + margin: 0 20px; + padding: 30px 0 36px; + } + .leaderboard-preview-copy h2 { + font-size: 42px; + margin-bottom: 14px; + } + .leaderboard-preview-copy h2 br { + display: none; + } + .leaderboard-kicker { + margin-bottom: 14px; + } + .leaderboard-preview-copy p br { + display: none; + } + .leaderboard-preview-copy p { + margin-bottom: 15px; + } + .board-heading { + padding: 16px; + } + .board-heading > span:last-child { + font-size: 9px; + } + .compact .leaderboard-table th, + .compact .leaderboard-table td { + padding: 12px 13px; + } + .compact .leaderboard-table .rank-column { + width: 43px; + padding-right: 0; + } + .compact .leaderboard-player { + gap: 10px; + } + .compact .score-column strong { + font-size: 23px; + } + .board-footnote { + padding: 12px 13px; + font-size: 9px; + } + .home-panels { + padding-top: 32px; + gap: 32px; + } + .home-panel + .home-panel { + padding: 30px 0 0; + border: 0; + border-top: 1px solid #2b3841; + } + .home-page .card-body { + padding: 12px 10px; + } + .home-page .card-body p { + display: none; + } + .home-page .card-meta { + font-size: 9px; + } + .leaderboard-page { + padding-top: 24px; + } + .leaderboard-back { + margin-bottom: 26px; + } + .leaderboard-intro { + gap: 12px; + align-items: flex-start; + } + .leaderboard-intro h1 { + font-size: 46px; + } + .leaderboard-intro .lead { + font-size: 12px; + } + .intro-trophy { + width: 52px; + height: 52px; + margin: 28px 0 0; + filter: drop-shadow(3px 3px #04080c); + } + .leaderboard-toolbar { + margin-top: 26px; + gap: 8px; + } + .leaderboard-filters { + gap: 3px; + font-size: 11px; + } + .leaderboard-filters a { + padding: 7px 9px; + } + .leaderboard-period { + font-size: 9px; + } + .winners-podium { + gap: 6px; + margin: 32px auto 28px; + } + .podium-player { + padding: 0 3px 17px; + } + .podium-label { + font-size: 8px; + letter-spacing: 0.3px; + margin-bottom: 12px; + } + .podium-player .player-avatar { + width: 36px; + height: 36px; + margin-bottom: 12px; + } + .podium-1 .player-avatar { + width: 44px; + height: 44px; + margin-bottom: 14px; + outline-offset: 3px; + } + .podium-player h2 { + font-size: 23px; + margin-bottom: 7px; + } + .podium-player p { + font-size: 23px; + } + .podium-player p span { + display: block; + font-size: 9px; + margin-top: 5px; + } + .podium-block { + height: 61px; + gap: 9px; + } + .podium-block > span { + font-size: 34px; + } + .podium-1 .podium-block { + height: 83px; + } + .podium-1 .podium-block .pixel-trophy { + width: 23px; + height: 23px; + } + .podium-3 .podium-block { + height: 46px; + } + .rankings-board { + margin-top: 24px; + } + .rankings-heading { + padding: 18px 14px; + flex-wrap: wrap; + gap: 18px; + } + .rankings-heading h2 { + font-size: 29px; + } + .leaderboard-search { + max-width: none; + width: 100%; + } + .leaderboard-table th, + .leaderboard-table td { + padding: 14px 12px; + } + .leaderboard-table .rank-column { + width: 40px; + padding-right: 0; + } + .leaderboard-table .average-column { + display: none; + } + .leaderboard-table .games-column { + padding-left: 6px; + padding-right: 6px; + } + .leaderboard-player { + gap: 9px; + } + .leaderboard-table .player-avatar { + width: 28px; + height: 28px; + } + .leaderboard-table .player-name { + font-size: 11px; + } + .leaderboard-table .score-column strong { + font-size: 24px; + } + .score-unit { + display: none; + } + .you-label { + display: block; + margin: 4px 0 0; + width: fit-content; + } + .search-summary { + padding: 12px 14px; + font-size: 11px; + } + .leaderboard-pagination { + padding: 16px 14px; + flex-wrap: wrap; + gap: 15px; + } + .leaderboard-pagination nav { + width: 100%; + justify-content: space-between; + gap: 10px; + } + .leaderboard-rules { + grid-template-columns: 1fr; + gap: 8px; + margin-top: 36px; + padding-top: 26px; + } + .leaderboard-rules h2 { + font-size: 30px; + max-width: none; + margin-bottom: 12px; + } + .leaderboard-empty { + padding: 36px 18px; + } +} diff --git a/app/leaderboard/loading.tsx b/app/leaderboard/loading.tsx new file mode 100644 index 0000000..d430ca3 --- /dev/null +++ b/app/leaderboard/loading.tsx @@ -0,0 +1,24 @@ +export default function LoadingLeaderboard() { + return ( +
+ THE EXPLORERS’ BOARD +

Every block counts.

+

+ Fetching the explorers’ scores… +

+ +
+ ); +} diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx new file mode 100644 index 0000000..fcc6810 --- /dev/null +++ b/app/leaderboard/page.tsx @@ -0,0 +1,217 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { ArrowLeft, ArrowRight, Search } from "lucide-react"; +import { Header, Footer } from "@/components/Shell"; +import { + LeaderboardEmpty, + LeaderboardTable, + WinnersPodium, +} from "@/components/Leaderboard"; +import { PixelTrophy } from "@/components/PixelArt"; +import { leaderboard } from "@/lib/leaderboard"; +import { user } from "@/lib/supabase"; +import { + formatScore, + leaderboardDifficulties, + leaderboardFilters, + leaderboardHref, +} from "@/shared/leaderboard"; + +export const dynamic = "force-dynamic"; +export const metadata: Metadata = { + title: "Leaderboard", + description: + "Meet MineGeo’s top explorers. Compare the best five-round scores across easy, normal, and hard Minecraft expeditions.", +}; + +export default async function LeaderboardPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const { difficulty, search, page } = leaderboardFilters(await searchParams); + const [data, account] = await Promise.all([ + leaderboard({ difficulty, search, page }), + user(), + ]); + return ( + <> +
+
+ + Back to home + +
+
+ THE EXPLORERS’ BOARD +

Every block counts.

+

+ The sharpest eyes in the Overworld. +
+ Five rounds. 25,000 possible points. Where do you stand? +

+
+ +
+
+ + All time +
+ {data ? ( + <> + {data.leaders.length > 0 && !search && data.page === 1 && ( + + )} +
+
+
+

The rankings

+

+ {formatScore(data.total_players)}{" "} + {data.total_players === 1 ? "explorer" : "explorers"} ·{" "} + {formatScore(data.total_games)} completed{" "} + {data.total_games === 1 ? "expedition" : "expeditions"} +

+
+
+ {difficulty !== "all" && ( + + )} + + + +
+
+ {search && ( +
+ + {formatScore(data.matching_players)}{" "} + {data.matching_players === 1 ? "result" : "results"} for “ + {search}” + + Clear search × +
+ )} + {data.entries.length ? ( + + ) : ( + + )} + {data.matching_players > 0 && ( +
+ + Showing {formatScore((data.page - 1) * 25 + 1)}– + {formatScore( + Math.min(data.page * 25, data.matching_players), + )}{" "} + of {formatScore(data.matching_players)} + + +
+ )} +
+ + ) : ( + + )} + +
+