feat: add lottery reset functionality in admin user actions and API
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m20s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m20s
This commit is contained in:
@@ -19,6 +19,7 @@ export function AdminUserActions({ user }: { user: UserData }) {
|
|||||||
const [points, setPoints] = useState(String(user.researchPoints))
|
const [points, setPoints] = useState(String(user.researchPoints))
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [resetUrl, setResetUrl] = useState<string | null>(null)
|
const [resetUrl, setResetUrl] = useState<string | null>(null)
|
||||||
|
const [lotteryReset, setLotteryReset] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
@@ -57,6 +58,23 @@ export function AdminUserActions({ user }: { user: UserData }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleResetLottery() {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
const res = await fetch(`/api/admin/users/${user.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ resetLotteryAt: true }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
setLoading(false)
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(data.error ?? 'Reset failed.')
|
||||||
|
} else {
|
||||||
|
setLotteryReset(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -103,7 +121,22 @@ export function AdminUserActions({ user }: { user: UserData }) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Reset URL section */}
|
{/* Lucky Dip reset */}
|
||||||
|
<div className="border-t border-surface-border pt-4">
|
||||||
|
<p className="text-sm text-slate-400 mb-2">Lucky Dip</p>
|
||||||
|
<button
|
||||||
|
onClick={handleResetLottery}
|
||||||
|
disabled={loading || lotteryReset}
|
||||||
|
className="text-sm bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-400 border border-indigo-500/30 px-4 py-1.5 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{lotteryReset ? 'Reset ✓' : 'Reset today\'s play'}
|
||||||
|
</button>
|
||||||
|
{lotteryReset && (
|
||||||
|
<p className="text-xs text-slate-500 mt-1">Player can play again today.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password reset */}
|
||||||
<div className="border-t border-surface-border pt-4">
|
<div className="border-t border-surface-border pt-4">
|
||||||
<p className="text-sm text-slate-400 mb-2">Password reset</p>
|
<p className="text-sm text-slate-400 mb-2">Password reset</p>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const schema = z.object({
|
|||||||
balance: z.number().min(0).optional(),
|
balance: z.number().min(0).optional(),
|
||||||
researchPoints: z.number().int().min(0).optional(),
|
researchPoints: z.number().int().min(0).optional(),
|
||||||
isAdmin: z.boolean().optional(),
|
isAdmin: z.boolean().optional(),
|
||||||
|
resetLotteryAt: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function PATCH(req: NextRequest, { params }: { params: { userId: string } }) {
|
export async function PATCH(req: NextRequest, { params }: { params: { userId: string } }) {
|
||||||
@@ -22,9 +23,14 @@ export async function PATCH(req: NextRequest, { params }: { params: { userId: st
|
|||||||
return NextResponse.json({ error: 'Invalid request.' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid request.' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { resetLotteryAt, ...rest } = parsed.data
|
||||||
|
|
||||||
const updated = await prisma.user.update({
|
const updated = await prisma.user.update({
|
||||||
where: { id: params.userId },
|
where: { id: params.userId },
|
||||||
data: parsed.data,
|
data: {
|
||||||
|
...rest,
|
||||||
|
...(resetLotteryAt ? { lastLotteryAt: null } : {}),
|
||||||
|
},
|
||||||
select: { id: true, username: true, balance: true, researchPoints: true, isAdmin: true },
|
select: { id: true, username: true, balance: true, researchPoints: true, isAdmin: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useSession, signOut } from 'next-auth/react'
|
import { useSession, signOut } from 'next-auth/react'
|
||||||
import { TrendingUp, Search, User, LogOut, Shield, Trophy, BarChart2 } from 'lucide-react'
|
import { TrendingUp, Search, User, LogOut, Shield, Trophy } from 'lucide-react'
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { formatCurrency } from '@/lib/utils'
|
import { formatCurrency } from '@/lib/utils'
|
||||||
@@ -61,14 +61,6 @@ export function Navbar() {
|
|||||||
<span className="font-bold text-lg hidden sm:block">HashEx</span>
|
<span className="font-bold text-lg hidden sm:block">HashEx</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Markets link */}
|
|
||||||
<Link
|
|
||||||
href="/stocks"
|
|
||||||
className="text-slate-400 hover:text-slate-200 transition-colors text-sm hidden sm:block shrink-0"
|
|
||||||
>
|
|
||||||
Markets
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<form onSubmit={handleSearch} className="flex-1 max-w-md">
|
<form onSubmit={handleSearch} className="flex-1 max-w-md">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
Reference in New Issue
Block a user