feat: implement balance tier system and update research points allocation in maintenance worker
This commit is contained in:
+33
-20
@@ -5,8 +5,7 @@ import { Loader2, Dices, CheckCircle2 } from 'lucide-react'
|
|||||||
import { formatCurrency } from '@/lib/utils'
|
import { formatCurrency } from '@/lib/utils'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { GRID_SIZE, PRIZE_MAP } from '@/lib/lottery'
|
||||||
const GRID_SIZE = 25
|
|
||||||
|
|
||||||
type PickResult = {
|
type PickResult = {
|
||||||
box: number
|
box: number
|
||||||
@@ -86,11 +85,24 @@ export default function LotteryPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const prizeLabels: Record<number, string> = {
|
const PRIZE_EMOJIS = ['🏆', '🥈', '🎁', '✨', '⭐']
|
||||||
200: '🏆 $200',
|
const uniquePrizes = [...new Set(Object.values(PRIZE_MAP))].filter(v => v > 0).sort((a, b) => b - a)
|
||||||
50: '🥈 $50',
|
const prizeCounts = uniquePrizes.reduce<Record<number, number>>((acc, amt) => {
|
||||||
10: '🎁 $10',
|
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<number, string> = Object.fromEntries(
|
||||||
|
uniquePrizes.map((amt, i) => [amt, `${PRIZE_EMOJIS[i] ?? '🎁'} ${formatCurrency(amt)}`])
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl mx-auto space-y-8">
|
<div className="max-w-xl mx-auto space-y-8">
|
||||||
@@ -107,21 +119,22 @@ export default function LotteryPage() {
|
|||||||
{/* Prize table */}
|
{/* Prize table */}
|
||||||
<div className="bg-surface-card border border-surface-border rounded-xl p-4">
|
<div className="bg-surface-card border border-surface-border rounded-xl p-4">
|
||||||
<p className="text-xs text-slate-500 mb-3 font-medium uppercase tracking-wider">Prize pool</p>
|
<p className="text-xs text-slate-500 mb-3 font-medium uppercase tracking-wider">Prize pool</p>
|
||||||
<div className="grid grid-cols-3 gap-2 text-sm text-center">
|
<div
|
||||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-2">
|
className="grid gap-2 text-sm text-center"
|
||||||
<p className="font-bold text-amber-400">$200</p>
|
style={{ gridTemplateColumns: `repeat(${uniquePrizes.length}, 1fr)` }}
|
||||||
<p className="text-xs text-slate-500">×1 box</p>
|
>
|
||||||
|
{uniquePrizes.map((amt, i) => {
|
||||||
|
const color = TIER_COLORS[i] ?? TIER_COLORS[TIER_COLORS.length - 1]
|
||||||
|
const count = prizeCounts[amt]
|
||||||
|
return (
|
||||||
|
<div key={amt} className={`${color.bg} border ${color.border} rounded-lg p-2`}>
|
||||||
|
<p className={`font-bold ${color.text}`}>{formatCurrency(amt)}</p>
|
||||||
|
<p className="text-xs text-slate-500">×{count} box{count !== 1 ? 'es' : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-500/10 border border-slate-500/20 rounded-lg p-2">
|
)
|
||||||
<p className="font-bold text-slate-300">$50</p>
|
})}
|
||||||
<p className="text-xs text-slate-500">×2 boxes</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-lg p-2">
|
<p className="text-xs text-slate-600 mt-2 text-center">All remaining {emptyBoxCount} boxes: $0</p>
|
||||||
<p className="font-bold text-emerald-400">$10</p>
|
|
||||||
<p className="text-xs text-slate-500">×3 boxes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-600 mt-2 text-center">Remaining 19 boxes: $0</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Result banner */}
|
{/* Result banner */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth'
|
|||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { formatCurrency, formatNumber, pnlColor, formatPnl } from '@/lib/utils'
|
import { formatCurrency, formatNumber, pnlColor, formatPnl } from '@/lib/utils'
|
||||||
|
import { getBalanceTier } from '@/lib/pricing'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { TrendingUp, TrendingDown, Coins } from 'lucide-react'
|
import { TrendingUp, TrendingDown, Coins } from 'lucide-react'
|
||||||
import ChangePasswordForm from './ChangePasswordForm'
|
import ChangePasswordForm from './ChangePasswordForm'
|
||||||
@@ -74,9 +75,28 @@ export default async function ProfilePage({ params }: Props) {
|
|||||||
<p className="text-slate-500 text-sm">@{user.username}</p>
|
<p className="text-slate-500 text-sm">@{user.username}</p>
|
||||||
)}
|
)}
|
||||||
{isOwn && (
|
{isOwn && (
|
||||||
<p className="text-slate-400 text-sm mt-1">
|
<div className="mt-1 text-sm text-slate-400 space-y-0.5">
|
||||||
|
{(() => {
|
||||||
|
const tier = getBalanceTier(user.balance)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<span className="text-slate-300 font-medium">Tier {tier.level}</span>
|
||||||
|
<span className="text-slate-600 mx-1.5">·</span>
|
||||||
|
{tier.pointsPerDay} research pt{tier.pointsPerDay !== 1 ? 's' : ''}/day
|
||||||
|
{tier.nextThreshold && (
|
||||||
|
<span className="text-slate-600 text-xs ml-1.5">
|
||||||
|
(next tier at {formatCurrency(tier.nextThreshold)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
{user.researchPoints} research point{user.researchPoints !== 1 ? 's' : ''} available
|
{user.researchPoints} research point{user.researchPoints !== 1 ? 's' : ''} available
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ export function dailyResearchPoints(balance: number): number {
|
|||||||
return 1
|
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.
|
* Calculate the cost/proceeds and realized P&L for a trade.
|
||||||
*
|
*
|
||||||
|
|||||||
+6
-2
@@ -170,14 +170,18 @@ const maintenanceWorker = new Worker(
|
|||||||
async (job) => {
|
async (job) => {
|
||||||
console.log(`[maintenance] running daily maintenance (job ${job.id})`)
|
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) {
|
for (const user of users) {
|
||||||
const points = dailyResearchPoints(user.balance)
|
const points = dailyResearchPoints(user.balance)
|
||||||
|
const newTotal = Math.min(user.researchPoints + points, MAX_RESEARCH_POINTS)
|
||||||
|
if (newTotal !== user.researchPoints) {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { researchPoints: { increment: points } },
|
data: { researchPoints: newTotal },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[maintenance] awarded research points to ${users.length} users`)
|
console.log(`[maintenance] awarded research points to ${users.length} users`)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user