diff --git a/messages/en.json b/messages/en.json index 8769a61..b472350 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/zh.json b/messages/zh.json index dbf150b..2b84855 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -141,7 +141,14 @@ "balance": "余额", "showing": "显示", "of": "的", - "transactions": "条记录" + "transactions": "条记录", + "topUp": "充值", + "enterAmount": "请输入充值金额", + "quickAmounts": "快捷金额", + "topUpNote": "注意:这是开发环境,不会进行真实的支付处理。", + "processing": "处理中...", + "topUpNow": "立即充值", + "cancel": "取消" }, "studio": { "title": "AI 提示词工作室", diff --git a/src/app/api/credits/topup/route.ts b/src/app/api/credits/topup/route.ts new file mode 100644 index 0000000..5103427 --- /dev/null +++ b/src/app/api/credits/topup/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/src/app/credits/page.tsx b/src/app/credits/page.tsx index be31787..940afcd 100644 --- a/src/app/credits/page.tsx +++ b/src/app/credits/page.tsx @@ -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 (
@@ -199,68 +250,96 @@ export default function CreditsPage() { {/* Stats Cards */} {stats && ( -
- -
-
- +
+ {/* Balance Card with Top-up Button */} +
+ +
+
+
+ +
+
+

{t('currentBalance')}

+

+ ${stats.currentBalance.toFixed(2)} +

+
+
-
-

{t('currentBalance')}

-

- ${stats.currentBalance.toFixed(2)} -

-
-
- + + + +
- -
-
- + {/* Other Stats */} +
+ +
+
+ +
+
+

{t('totalEarned')}

+

${stats.totalEarned.toFixed(2)}

+
-
-

{t('totalEarned')}

-

${stats.totalEarned.toFixed(2)}

-
-
- + - -
-
- + +
+
+ +
+
+

{t('totalEarned')}

+

${stats.totalEarned.toFixed(2)}

+
-
-

{t('totalSpent')}

-

${stats.totalSpent.toFixed(2)}

-
-
- + - -
-
- + +
+
+ +
+
+

{t('totalSpent')}

+

${stats.totalSpent.toFixed(2)}

+
-
-

{t('thisMonthEarned')}

-

${stats.thisMonthEarned.toFixed(2)}

-
-
- + - -
-
- + +
+
+ +
+
+

{t('thisMonthEarned')}

+

${stats.thisMonthEarned.toFixed(2)}

+
-
-

{t('thisMonthSpent')}

-

${stats.thisMonthSpent.toFixed(2)}

+ + + +
+
+ +
+
+

{t('thisMonthSpent')}

+

${stats.thisMonthSpent.toFixed(2)}

+
-
-
+ +
)} @@ -442,6 +521,96 @@ export default function CreditsPage() { )}
+ + {/* Top-up Modal */} + {showTopUpModal && ( +
+
+
+

{t('topUp')}

+ +
+ +
+
+ +
+ + 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" + /> +
+
+ +
+

{t('quickAmounts')}

+
+ {predefinedAmounts.map((amount) => ( + + ))} +
+
+ +
+

+ {t('topUpNote')} +

+
+
+ +
+ + +
+
+
+ )}
)