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.
55 lines
1.6 KiB
JavaScript
55 lines
1.6 KiB
JavaScript
/**
|
|
* Time-bucket keys for leaderboards, computed in UTC so every player shares the
|
|
* same boundaries worldwide. Each key is a plain string denormalized onto a
|
|
* score document, letting a "Today / Week / Month" query be a cheap equality
|
|
* filter instead of a range scan.
|
|
*/
|
|
|
|
const pad = n => (n < 10 ? `0${n}` : `${n}`)
|
|
|
|
/**
|
|
* Calendar-day key, e.g. `2026-07-03`.
|
|
* @param {Date} date
|
|
* @returns {String}
|
|
*/
|
|
export const dayKey = date => {
|
|
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`
|
|
}
|
|
|
|
/**
|
|
* Calendar-month key, e.g. `2026-07`.
|
|
* @param {Date} date
|
|
* @returns {String}
|
|
*/
|
|
export const monthKey = date => {
|
|
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}`
|
|
}
|
|
|
|
/**
|
|
* ISO-8601 week key, e.g. `2026-W27`. Weeks start Monday and the week-numbering
|
|
* year can differ from the calendar year around January 1st, so we derive the
|
|
* year from the week's Thursday.
|
|
* @param {Date} date
|
|
* @returns {String}
|
|
*/
|
|
export const weekKey = date => {
|
|
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
|
const dayNum = d.getUTCDay() || 7
|
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
const week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
|
|
return `${d.getUTCFullYear()}-W${pad(week)}`
|
|
}
|
|
|
|
/**
|
|
* All three bucket keys for a moment in time.
|
|
* @param {Date} date
|
|
* @returns {{ day: String, week: String, month: String }}
|
|
*/
|
|
export const buckets = date => {
|
|
return {
|
|
day: dayKey(date),
|
|
week: weekKey(date),
|
|
month: monthKey(date)
|
|
}
|
|
}
|