add topup
This commit is contained in:
parent
a6a227597f
commit
b3a3e2b456
@ -141,7 +141,14 @@
|
||||
"balance": "Balance",
|
||||
"showing": "Showing",
|
||||
"of": "of",
|
||||
"transactions": "transactions"
|
||||
"transactions": "transactions",
|
||||
"topUp": "Top Up",
|
||||
"enterAmount": "Enter amount to top up",
|
||||
"quickAmounts": "Quick amounts",
|
||||
"topUpNote": "Note: This is a development environment. No real payment will be processed.",
|
||||
"processing": "Processing...",
|
||||
"topUpNow": "Top Up Now",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI Prompt Studio",
|
||||
|
@ -141,7 +141,14 @@
|
||||
"balance": "余额",
|
||||
"showing": "显示",
|
||||
"of": "的",
|
||||
"transactions": "条记录"
|
||||
"transactions": "条记录",
|
||||
"topUp": "充值",
|
||||
"enterAmount": "请输入充值金额",
|
||||
"quickAmounts": "快捷金额",
|
||||
"topUpNote": "注意:这是开发环境,不会进行真实的支付处理。",
|
||||
"processing": "处理中...",
|
||||
"topUpNow": "立即充值",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI 提示词工作室",
|
||||
|
71
src/app/api/credits/topup/route.ts
Normal file
71
src/app/api/credits/topup/route.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
||||
import { addCredit } from '@/lib/services/credit'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { amount } = await request.json()
|
||||
|
||||
// 验证金额
|
||||
if (!amount || amount <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid amount. Amount must be greater than 0.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (amount > 1000) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Amount too large. Maximum top-up amount is $1000.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// 获取当前用户
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查是否是开发环境
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
if (isDevelopment) {
|
||||
// 开发环境:模拟充值成功
|
||||
console.log(`🧪 Development mode: Simulating top-up of $${amount} for user ${user.id}`)
|
||||
|
||||
const creditTransaction = await addCredit(
|
||||
user.id,
|
||||
amount,
|
||||
'user_purchase',
|
||||
'development',
|
||||
null,
|
||||
null,
|
||||
`Development top-up: $${amount}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully topped up $${amount}`,
|
||||
transaction: creditTransaction
|
||||
})
|
||||
} else {
|
||||
// 生产环境:集成真实的支付网关
|
||||
// 这里可以集成 Stripe, PayPal 等支付服务
|
||||
return NextResponse.json(
|
||||
{ error: 'Payment integration not implemented yet. Please contact support.' },
|
||||
{ status: 501 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Top-up failed:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process top-up' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@ -19,7 +19,9 @@ import {
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Minus,
|
||||
Zap
|
||||
Zap,
|
||||
DollarSign,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
|
||||
interface CreditTransaction {
|
||||
@ -65,6 +67,11 @@ export default function CreditsPage() {
|
||||
const [sortBy, setSortBy] = useState<'createdAt' | 'amount' | 'balance'>('createdAt')
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
||||
|
||||
// Top-up modal state
|
||||
const [showTopUpModal, setShowTopUpModal] = useState(false)
|
||||
const [topUpAmount, setTopUpAmount] = useState('')
|
||||
const [isTopUpLoading, setIsTopUpLoading] = useState(false)
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
|
||||
const loadCreditData = useCallback(async () => {
|
||||
@ -173,6 +180,50 @@ export default function CreditsPage() {
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const handleTopUp = async () => {
|
||||
const amount = parseFloat(topUpAmount)
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Please enter a valid amount greater than 0')
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > 1000) {
|
||||
alert('Maximum top-up amount is $1000')
|
||||
return
|
||||
}
|
||||
|
||||
setIsTopUpLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/credits/topup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ amount }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
alert(`Successfully topped up $${amount}!`)
|
||||
setShowTopUpModal(false)
|
||||
setTopUpAmount('')
|
||||
// Reload credit data to reflect the new balance
|
||||
loadCreditData()
|
||||
} else {
|
||||
alert(data.error || 'Failed to process top-up')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Top-up error:', error)
|
||||
alert('Failed to process top-up. Please try again.')
|
||||
} finally {
|
||||
setIsTopUpLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const predefinedAmounts = [5, 10, 25, 50, 100]
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
@ -199,68 +250,96 @@ export default function CreditsPage() {
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||
<Card className="p-6 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200/50 dark:border-green-800/50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-green-500">
|
||||
<CreditCard className="w-5 h-5 text-white" />
|
||||
<div className="space-y-4 mb-8">
|
||||
{/* Balance Card with Top-up Button */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-end">
|
||||
<Card className="flex-1 p-6 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200/50 dark:border-green-800/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-green-500">
|
||||
<CreditCard className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-700 dark:text-green-300">{t('currentBalance')}</p>
|
||||
<p className="text-2xl font-bold text-green-800 dark:text-green-200">
|
||||
${stats.currentBalance.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-700 dark:text-green-300">{t('currentBalance')}</p>
|
||||
<p className="text-2xl font-bold text-green-800 dark:text-green-200">
|
||||
${stats.currentBalance.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-blue-500">
|
||||
<TrendingUp className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('totalEarned')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.totalEarned.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Button
|
||||
onClick={() => setShowTopUpModal(true)}
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white px-6 py-2 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('topUp')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-red-500">
|
||||
<TrendingDown className="w-5 h-5 text-white" />
|
||||
{/* Other Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-blue-500">
|
||||
<TrendingUp className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('totalEarned')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.totalEarned.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('totalSpent')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.totalSpent.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-emerald-500">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-blue-500">
|
||||
<TrendingUp className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('totalEarned')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.totalEarned.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('thisMonthEarned')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.thisMonthEarned.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-orange-500">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-red-500">
|
||||
<TrendingDown className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('totalSpent')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.totalSpent.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('thisMonthSpent')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.thisMonthSpent.toFixed(2)}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-emerald-500">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('thisMonthEarned')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.thisMonthEarned.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-full bg-orange-500">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('thisMonthSpent')}</p>
|
||||
<p className="text-xl font-bold text-foreground">${stats.thisMonthSpent.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -442,6 +521,96 @@ export default function CreditsPage() {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Top-up Modal */}
|
||||
{showTopUpModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-background rounded-lg shadow-xl max-w-md w-full">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 className="text-xl font-semibold text-foreground">{t('topUp')}</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowTopUpModal(false)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
{t('enterAmount')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<DollarSign className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="number"
|
||||
value={topUpAmount}
|
||||
onChange={(e) => setTopUpAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
min="0.01"
|
||||
max="1000"
|
||||
step="0.01"
|
||||
className="w-full pl-10 pr-4 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground mb-2">{t('quickAmounts')}</p>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{predefinedAmounts.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTopUpAmount(amount.toString())}
|
||||
className="text-xs"
|
||||
>
|
||||
${amount}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 p-3 rounded-md">
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
{t('topUpNote')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 p-6 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowTopUpModal(false)}
|
||||
disabled={isTopUpLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTopUp}
|
||||
disabled={isTopUpLoading || !topUpAmount || parseFloat(topUpAmount) <= 0}
|
||||
className="flex-1 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700"
|
||||
>
|
||||
{isTopUpLoading ? (
|
||||
<>
|
||||
<LoadingSpinner className="w-4 h-4 mr-2" />
|
||||
{t('processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('topUpNow')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user