dd6a3a3bfa
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
71 lines
3.9 KiB
JavaScript
71 lines
3.9 KiB
JavaScript
// Leaderboard, badges and share links. All derived from the payout ledger; nothing new is stored.
|
||
//
|
||
// A "find" is any non-failed payout (paid, sent, due or queued): the hunter did the work. POL counts
|
||
// paid drips only, so the board never shows money that has not left the faucet. Days and weeks are
|
||
// Central (ctDay), the same clock as the pool.
|
||
//
|
||
// settings.leaderboardExclude: member ids kept off the public boards (default: the company account #1).
|
||
'use strict';
|
||
const store = require('./store');
|
||
const { ctDay } = require('./missions');
|
||
const rewards = require('./rewards');
|
||
const missions = require('./missions');
|
||
|
||
const BADGES = [
|
||
{ id: 'first', name: 'First Find', icon: '\u{1F3AF}', why: 'your first code claimed' },
|
||
{ id: 'hunter', name: 'Hunter', icon: '\u{1F9ED}', why: 'five finds' },
|
||
{ id: 'tracker', name: 'Tracker', icon: '\u{1F526}', why: 'twenty finds' },
|
||
{ id: 'sweep', name: 'Full Sweep', icon: '\u{1F5FA}️', why: 'a find on every site' },
|
||
{ id: 'lucky', name: 'Lucky Find', icon: '\u{1F48E}', why: 'one drip of 0.4 POL or more' },
|
||
{ id: 'streak', name: 'On a Streak', icon: '\u{1F525}', why: 'finds on three days in a row' },
|
||
];
|
||
|
||
function excluded() { const s = rewards.settings(); const list = Array.isArray(s.leaderboardExclude) ? s.leaderboardExclude : [1]; return new Set(list.map(Number)); }
|
||
function finds() { return store.read('payouts', []).filter(p => p.status !== 'failed'); }
|
||
|
||
// Central day arithmetic on 'YYYY-MM-DD' keys
|
||
function dayMinus(dayKey, n) { const [y, m, d] = dayKey.split('-').map(Number); const t = Date.UTC(y, m - 1, d) - n * 86400000; return new Date(t).toISOString().slice(0, 10); }
|
||
function inPeriod(p, period, today) {
|
||
if (period === 'all') return true;
|
||
if (period === 'month') return p.day.slice(0, 7) === today.slice(0, 7);
|
||
return p.day >= dayMinus(today, 6); // week: the last seven Central days including today
|
||
}
|
||
|
||
function badgesFor(memberId, list) {
|
||
const mine = (list || finds()).filter(p => p.memberId === Number(memberId));
|
||
if (!mine.length) return [];
|
||
const out = new Set();
|
||
out.add('first');
|
||
if (mine.length >= 5) out.add('hunter');
|
||
if (mine.length >= 20) out.add('tracker');
|
||
const sites = new Set(missions.list().filter(m => m.active !== false).map(m => m.site));
|
||
if (sites.size && [...sites].every(s => mine.some(p => p.site === s))) out.add('sweep');
|
||
if (mine.some(p => p.pol >= 0.4)) out.add('lucky');
|
||
const days = new Set(mine.map(p => p.day));
|
||
for (const d of days) { if (days.has(dayMinus(d, 1)) && days.has(dayMinus(d, 2))) { out.add('streak'); break; } }
|
||
return BADGES.filter(b => out.has(b.id));
|
||
}
|
||
|
||
function leaderboard(period, limit) {
|
||
const today = ctDay(); const ex = excluded(); const all = finds();
|
||
const by = new Map();
|
||
for (const p of all) {
|
||
if (ex.has(p.memberId) || !inPeriod(p, period, today)) continue;
|
||
const r = by.get(p.memberId) || { memberId: p.memberId, who: p.username ? '@' + p.username : '#' + p.memberId, finds: 0, pol: 0, last: 0 };
|
||
r.finds++; if (p.status === 'paid') r.pol += p.pol; r.last = Math.max(r.last, p.at); by.set(p.memberId, r);
|
||
}
|
||
const rows = [...by.values()].sort((a, b) => b.finds - a.finds || b.pol - a.pol || a.last - b.last);
|
||
return rows.slice(0, limit || 25).map((r, i) => Object.assign(r, { rank: i + 1, pol: Math.round(r.pol * 10000) / 10000, badges: badgesFor(r.memberId, all).map(b => b.icon) }));
|
||
}
|
||
|
||
function rankOf(memberId, period) {
|
||
const rows = leaderboard(period, 100000); const i = rows.findIndex(r => r.memberId === Number(memberId));
|
||
return i < 0 ? null : { rank: i + 1, of: rows.length, finds: rows[i].finds, pol: rows[i].pol };
|
||
}
|
||
|
||
// the member's share link: PolHunter's landing with their IAP referral, which the landing turns into
|
||
// their instantadpay.com/join/<ref> link for everyone who signs up from it
|
||
function shareLink(site, me) { const ref = me.username || me.memberId; return site + '/?r=' + encodeURIComponent(String(ref)); }
|
||
|
||
module.exports = { BADGES, badgesFor, leaderboard, rankOf, shareLink };
|