mnswpr/app/main.js
Ayo 4fa88fb431 feat: reusable, time-windowed leaderboard (@cozy-games/leaderboard)
Extract the leaderboard into a generic, backend-agnostic package and add
rolling Today/Week/Month/All-Time windows, a web component, and local-first
development against the Firestore emulator.

Package (leaderboard/):
- LeaderBoardService: backend-agnostic core via a storage-adapter seam, with
  Firebase and Supabase adapters (Supabase client injected, no added dep)
- Rolling time windows (last 24h / 7d / 30d) with hover tooltips; top-N by score
- <cozy-leaderboard> web component built on web-component-base: compose the UI
  in HTML, configure the backend once in JS
- Every user-facing string is configurable (labels, tooltips, empty/loading/
  error messages, anonymous name) so i18n lives in the app; README + CONFIGURATION

App (mnswpr.com):
- Compose the board declaratively in index.html via <cozy-leaderboard>
- Nickname + randomized greeting bar; score submission through the element
- Legends: the current all-time leaders frozen into a static /legends page
- Firebase config and leaderboard namespace via Vite env vars; emulator-first
  local dev (VITE_FIRESTORE_EMULATOR)

Firebase schema-as-code:
- firebase.json, .firebaserc, firestore.rules (public reads, create-only scores,
  no client updates/deletes, namespace-generalized), empty indexes (rolling
  windows need none)
- prod (mw-*) vs dev/test (mw-test-*) separation by collection namespace
- emulator config, seed script, and docs (firebase-leaderboards.md,
  leaderboard-env-migration.md, AYO.md)

Utils/tests: UTC date-bucket helper (retained as metadata) with Vitest coverage.

Reviewed-on: https://git.ayo.run/ayo/mnswpr/pulls/1
Co-authored-by: Ayo <ayo@ayco.io>
Co-committed-by: Ayo <ayo@ayco.io>
2026-07-03 13:42:03 +02:00

73 lines
3 KiB
JavaScript

import mnswpr from '@ayo-run/mnswpr/mnswpr.js'
import '@ayo-run/mnswpr/mnswpr.css'
import * as pkg from '@ayo-run/mnswpr/package.json'
import { configureLeaderboard } from '@cozy-games/leaderboard/leaderboard-element.js'
import { FirebaseAdapter } from '@cozy-games/leaderboard/adapters/firebase.js'
import { NicknameService } from './modules/nickname/nickname.js'
import { UserService } from './modules/user/user.js'
// Firebase config comes from Vite env vars so dev and production point at
// different databases: dev values live in app/.env.development (committed —
// these keys are public, not secrets, and access is governed by
// firestore.rules); production values are supplied as Netlify env vars.
// https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public/37484053#37484053
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
}
const user = new UserService()
const nickname = new NicknameService()
// The leaderboard UI is composed declaratively as <cozy-leaderboard> in
// index.html; here we only wire its backend once. Namespace selects prod (`mw`)
// vs test (`mw-test`) collections in the same Firebase project, defaulting to
// test so a missing env var can never write into the production board.
// Local dev runs against the Firestore emulator by default (VITE_FIRESTORE_EMULATOR
// in app/.env.development); production builds always use real Firestore.
const useEmulator = ['1', 'true', 'yes'].includes(String(import.meta.env.VITE_FIRESTORE_EMULATOR))
configureLeaderboard({
adapter: new FirebaseAdapter({
firebaseConfig,
namespace: import.meta.env.VITE_LB_NAMESPACE || 'mw-test',
emulator: useEmulator ? { host: '127.0.0.1', port: 8080 } : undefined
})
})
const version = import.meta.env.MODE === 'development'
? 'dev'
: pkg.version
// Ask for a nickname on first visit and show the greeting bar.
nickname.ensure()
const greeting = document.getElementById('greeting')
if (greeting) nickname.render(greeting)
const board = document.querySelector('cozy-leaderboard')
const game = new mnswpr('app', version, {
// Point the board at the current level; the element re-renders reactively and
// keeps the selected duration tab.
levelChanged: (level) => {
board.setAttribute('title', `Best Times (${level.name})`)
board.setAttribute('category', level.id)
},
// Submit the finished game through the element's service.
gameDone: (game) => board.submit({
name: nickname.get(),
playerId: user.browserId,
score: game.time,
category: game.level,
time_stamp: game.time_stamp,
status: game.status,
meta: { isMobile: game.isMobile }
})
})
game.initialize()