Files
opengram/stargift-admin/frontend/src/pages/IssueStars.jsx
T
2026-06-05 00:53:35 +03:00

283 lines
15 KiB
React

import { useState, useEffect } from 'react'
import { Search, Coins, Sparkles, TrendingUp, User, Calendar, Hash } from 'lucide-react'
import toast from 'react-hot-toast'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'
export default function IssueStars() {
const [userId, setUserId] = useState('')
const [amount, setAmount] = useState('')
const [reason, setReason] = useState('')
const [loading, setLoading] = useState(false)
const [userInfo, setUserInfo] = useState(null)
const [recentTransactions, setRecentTransactions] = useState([])
useEffect(() => {
loadRecentTransactions()
}, [])
const loadRecentTransactions = async () => {
try {
const response = await fetch(`${API_URL}/stars/recent?limit=10`)
const data = await response.json()
if (data.success) {
setRecentTransactions(data.data)
}
} catch (error) {
console.error('Failed to load recent transactions:', error)
}
}
const searchUser = async () => {
if (!userId) return
try {
const response = await fetch(`${API_URL}/stars/user/${userId}`)
const data = await response.json()
if (data.success) {
setUserInfo(data.data)
toast.success(`User found: ${data.data.user.firstName || 'User ' + userId}`)
} else {
toast.error(data.error || 'User not found')
setUserInfo(null)
}
} catch (error) {
toast.error('User not found')
setUserInfo(null)
}
}
const issueStars = async (e) => {
e.preventDefault()
if (!userId || !amount || amount <= 0) {
toast.error('Please enter valid User ID and amount')
return
}
setLoading(true)
try {
const response = await fetch(`${API_URL}/stars/issue`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: parseInt(userId),
amount: parseInt(amount),
reason: reason || 'Admin issued stars'
})
})
const data = await response.json()
if (data.success) {
toast.success(`✨ Successfully issued ${amount} stars!`)
setAmount('')
setReason('')
loadRecentTransactions()
searchUser() // Refresh user info
} else {
toast.error(data.error || 'Failed to issue stars')
}
} catch (error) {
toast.error('Failed to issue stars')
} finally {
setLoading(false)
}
}
return (
<div className="space-y-6 animate-fade-in">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-heading font-bold text-fg flex items-center gap-3">
<div className="p-2.5 bg-gradient-to-br from-yellow to-orange-500 rounded-xl shadow-lg shadow-orange-500/20">
<Coins className="w-8 h-8 text-white" />
</div>
Issue Telegram Stars
</h1>
<p className="text-fg-muted mt-2">Grant stars to users directly from admin panel</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Form */}
<div className="lg:col-span-2 space-y-6">
{/* User Search Card */}
<div className="card p-6 border-border">
<h2 className="text-xl font-heading font-semibold text-fg mb-4 flex items-center gap-2">
<Search className="w-5 h-5 text-purple" />
Find User
</h2>
<div className="flex gap-3">
<div className="flex-1 relative">
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-fg-muted" />
<input
type="number"
value={userId}
onChange={(e) => setUserId(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && searchUser()}
placeholder="Enter User ID"
className="input w-full pl-12"
/>
</div>
<button
onClick={searchUser}
className="btn btn-primary flex items-center gap-2 px-6"
>
<Search className="w-5 h-5" />
Search
</button>
</div>
{/* User Info Display */}
{userInfo && (
<div className="mt-4 p-4 bg-muted/40 border border-border rounded-xl animate-scale-in">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-purple to-blue rounded-full flex items-center justify-center shadow-lg shadow-purple/20">
<User className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h3 className="text-fg font-semibold text-lg">
{userInfo.user.firstName} {userInfo.user.lastName || ''}
</h3>
<p className="text-fg-muted text-sm font-mono">
@{userInfo.user.userName || `user${userInfo.user.userId}`} · ID: {userInfo.user.userId}
</p>
</div>
<div className="text-right">
<p className="text-fg-muted text-xs uppercase tracking-wider mb-1">Current Balance</p>
<p className="text-2xl font-bold text-yellow flex items-center justify-end gap-1.5">
<Coins className="w-6 h-6 drop-shadow-sm" />
{userInfo.balance}
</p>
</div>
</div>
</div>
)}
</div>
{/* Issue Stars Form */}
<form onSubmit={issueStars} className="card p-6 border-border relative overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 p-32 bg-yellow/5 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none"></div>
<h2 className="text-xl font-heading font-semibold text-fg mb-6 flex items-center gap-2 relative z-10">
<Sparkles className="w-5 h-5 text-yellow" />
Issue Stars
</h2>
<div className="space-y-6 relative z-10">
<div>
<label className="block text-fg font-medium text-sm mb-2">
Amount of Stars
</label>
<div className="relative">
<Coins className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-yellow" />
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Enter amount"
min="1"
required
className="input w-full pl-12 text-lg font-medium"
/>
</div>
{/* Quick Amount Buttons */}
<div className="flex flex-wrap gap-2 mt-3">
{[50, 100, 500, 1000, 5000].map((amt) => (
<button
key={amt}
type="button"
onClick={() => setAmount(amt.toString())}
className="px-3 py-1.5 bg-muted border border-border hover:border-yellow hover:text-yellow rounded-lg text-sm text-fg-muted transition-all hover:-translate-y-0.5"
>
+{amt}
</button>
))}
</div>
</div>
<div>
<label className="block text-fg font-medium text-sm mb-2">
Reason (Optional)
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="e.g. Promotional bonus, Bug bounty reward, etc."
rows="3"
className="input w-full resize-none"
/>
</div>
<button
type="submit"
disabled={loading || !userId || !amount}
className="btn w-full bg-gradient-to-r from-yellow to-orange-500 hover:from-yellow/90 hover:to-orange-500/90 text-black font-bold text-lg py-4 shadow-xl shadow-orange-500/20 hover:shadow-orange-500/30 transition-all transform hover:scale-[1.02] disabled:opacity-50 disabled:scale-100 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{loading ? (
<>
<div className="w-5 h-5 border-2 border-black/30 border-t-black rounded-full animate-spin" />
Processing...
</>
) : (
<>
<Sparkles className="w-5 h-5" />
Issue {amount || '0'} Stars
</>
)}
</button>
</div>
</form>
</div>
{/* Recent Transactions Sidebar */}
<div className="lg:col-span-1">
<div className="card p-6 border-border sticky top-6 max-h-[calc(100vh-2rem)] flex flex-col">
<h2 className="text-xl font-heading font-semibold text-fg mb-4 flex items-center gap-2 shrink-0">
<TrendingUp className="w-5 h-5 text-success" />
Recent Transactions
</h2>
<div className="space-y-3 overflow-y-auto pr-2 custom-scrollbar flex-1">
{recentTransactions.length === 0 ? (
<div className="text-center py-10 bg-muted/20 rounded-xl border border-dashed border-border">
<div className="w-10 h-10 bg-muted rounded-full flex items-center justify-center mx-auto mb-3">
<TrendingUp className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-fg-muted font-medium">No transactions yet</p>
</div>
) : (
recentTransactions.map((tx, idx) => (
<div
key={idx}
className="p-3 bg-muted/20 border border-border rounded-xl hover:border-purple/50 hover:bg-muted/40 transition-colors group"
>
<div className="flex items-center justify-between mb-2">
<span className="text-fg font-medium flex items-center gap-1.5 text-sm">
<User className="w-3.5 h-3.5 text-fg-muted" />
User {tx.userId}
</span>
<span className={`font-bold text-sm ${tx.Amount > 0 ? 'text-success' : 'text-red'}`}>
{tx.Amount > 0 ? '+' : ''}{tx.Amount}
</span>
</div>
<p className="text-fg-muted text-xs line-clamp-1 mb-1.5 group-hover:text-fg transition-colors">{tx.Reason}</p>
<div className="flex items-center gap-1.5 text-fg-muted opacity-70 text-[10px]">
<Calendar className="w-3 h-3" />
{new Date(tx.Date).toLocaleString()}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
)
}