diff --git a/src/app/lottery/page.tsx b/src/app/lottery/page.tsx index 7c6a118..36e565f 100644 --- a/src/app/lottery/page.tsx +++ b/src/app/lottery/page.tsx @@ -5,8 +5,7 @@ import { Loader2, Dices, CheckCircle2 } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import { useSession } from 'next-auth/react' import Link from 'next/link' - -const GRID_SIZE = 25 +import { GRID_SIZE, PRIZE_MAP } from '@/lib/lottery' type PickResult = { box: number @@ -86,11 +85,24 @@ export default function LotteryPage() { ) } - const prizeLabels: Record = { - 200: '🏆 $200', - 50: '🥈 $50', - 10: '🎁 $10', - } + const PRIZE_EMOJIS = ['🏆', '🥈', '🎁', '✨', '⭐'] + const uniquePrizes = [...new Set(Object.values(PRIZE_MAP))].filter(v => v > 0).sort((a, b) => b - a) + const prizeCounts = uniquePrizes.reduce>((acc, amt) => { + acc[amt] = Object.values(PRIZE_MAP).filter(v => v === amt).length + return acc + }, {}) + const prizeBoxCount = Object.values(PRIZE_MAP).filter(v => v > 0).length + const emptyBoxCount = GRID_SIZE - prizeBoxCount + const TIER_COLORS = [ + { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400' }, + { bg: 'bg-slate-500/10', border: 'border-slate-500/20', text: 'text-slate-300' }, + { bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', text: 'text-emerald-400' }, + { bg: 'bg-indigo-500/10', border: 'border-indigo-500/20', text: 'text-indigo-400' }, + { bg: 'bg-pink-500/10', border: 'border-pink-500/20', text: 'text-pink-400' }, + ] + const prizeLabels: Record = Object.fromEntries( + uniquePrizes.map((amt, i) => [amt, `${PRIZE_EMOJIS[i] ?? '🎁'} ${formatCurrency(amt)}`]) + ) return (
@@ -107,21 +119,22 @@ export default function LotteryPage() { {/* Prize table */}

Prize pool

-
-
-

$200

-

×1 box

-
-
-

$50

-

×2 boxes

-
-
-

$10

-

×3 boxes

-
+
+ {uniquePrizes.map((amt, i) => { + const color = TIER_COLORS[i] ?? TIER_COLORS[TIER_COLORS.length - 1] + const count = prizeCounts[amt] + return ( +
+

{formatCurrency(amt)}

+

×{count} box{count !== 1 ? 'es' : ''}

+
+ ) + })}
-

Remaining 19 boxes: $0

+

All remaining {emptyBoxCount} boxes: $0

{/* Result banner */} diff --git a/src/app/profile/[username]/page.tsx b/src/app/profile/[username]/page.tsx index c55a7eb..290693f 100644 --- a/src/app/profile/[username]/page.tsx +++ b/src/app/profile/[username]/page.tsx @@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { notFound } from 'next/navigation' import { formatCurrency, formatNumber, pnlColor, formatPnl } from '@/lib/utils' +import { getBalanceTier } from '@/lib/pricing' import Link from 'next/link' import { TrendingUp, TrendingDown, Coins } from 'lucide-react' import ChangePasswordForm from './ChangePasswordForm' @@ -74,9 +75,28 @@ export default async function ProfilePage({ params }: Props) {

@{user.username}

)} {isOwn && ( -

- {user.researchPoints} research point{user.researchPoints !== 1 ? 's' : ''} available -

+
+ {(() => { + const tier = getBalanceTier(user.balance) + return ( + <> +

+ Tier {tier.level} + · + {tier.pointsPerDay} research pt{tier.pointsPerDay !== 1 ? 's' : ''}/day + {tier.nextThreshold && ( + + (next tier at {formatCurrency(tier.nextThreshold)}) + + )} +

+

+ {user.researchPoints} research point{user.researchPoints !== 1 ? 's' : ''} available +

+ + ) + })()} +
)}
diff --git a/src/lib/pricing.ts b/src/lib/pricing.ts index 7491713..f415753 100644 --- a/src/lib/pricing.ts +++ b/src/lib/pricing.ts @@ -25,6 +25,20 @@ export function dailyResearchPoints(balance: number): number { return 1 } +export interface BalanceTier { + level: number + pointsPerDay: number + nextThreshold: number | null +} + +/** Returns the tier info for a given balance. */ +export function getBalanceTier(balance: number): BalanceTier { + if (balance >= 1_000_000) return { level: 4, pointsPerDay: 5, nextThreshold: null } + if (balance >= 100_000) return { level: 3, pointsPerDay: 3, nextThreshold: 1_000_000 } + if (balance >= 10_000) return { level: 2, pointsPerDay: 2, nextThreshold: 100_000 } + return { level: 1, pointsPerDay: 1, nextThreshold: 10_000 } +} + /** * Calculate the cost/proceeds and realized P&L for a trade. * diff --git a/src/worker/index.ts b/src/worker/index.ts index 56cbf8e..04b8a4d 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -170,13 +170,17 @@ const maintenanceWorker = new Worker( async (job) => { console.log(`[maintenance] running daily maintenance (job ${job.id})`) - const users = await prisma.user.findMany({ select: { id: true, balance: true } }) + const MAX_RESEARCH_POINTS = 10 + const users = await prisma.user.findMany({ select: { id: true, balance: true, researchPoints: true } }) for (const user of users) { const points = dailyResearchPoints(user.balance) - await prisma.user.update({ - where: { id: user.id }, - data: { researchPoints: { increment: points } }, - }) + const newTotal = Math.min(user.researchPoints + points, MAX_RESEARCH_POINTS) + if (newTotal !== user.researchPoints) { + await prisma.user.update({ + where: { id: user.id }, + data: { researchPoints: newTotal }, + }) + } } console.log(`[maintenance] awarded research points to ${users.length} users`)