Skip to content

Commit 16ac552

Browse files
committed
feat: post detail with comments, profile edit, billing/credits page, comments API, profile update API
1 parent abf08dc commit 16ac552

5 files changed

Lines changed: 699 additions & 0 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import Link from 'next/link';
5+
import { useParams } from 'next/navigation';
6+
import { ArrowLeft, CreditCard, Zap, TrendingUp, ArrowUpRight, ArrowDownLeft, RefreshCw } from 'lucide-react';
7+
import { ThemeToggle } from '@/components/theme-toggle';
8+
9+
interface CreditData {
10+
id: string;
11+
balance: number;
12+
lifetime: number;
13+
transactions: {
14+
id: string;
15+
amount: number;
16+
type: string;
17+
description: string | null;
18+
createdAt: string;
19+
}[];
20+
}
21+
22+
export default function BillingPage() {
23+
const params = useParams();
24+
const locale = (params?.locale as string) || 'en';
25+
const [credits, setCredits] = useState<CreditData | null>(null);
26+
const [loading, setLoading] = useState(true);
27+
const [userId, setUserId] = useState<string | null>(null);
28+
29+
useEffect(() => {
30+
try {
31+
const stored = localStorage.getItem('user');
32+
if (stored) {
33+
const user = JSON.parse(stored);
34+
setUserId(user.id);
35+
fetchCredits(user.id);
36+
} else {
37+
setLoading(false);
38+
}
39+
} catch { setLoading(false); }
40+
}, []);
41+
42+
async function fetchCredits(uid: string) {
43+
try {
44+
const res = await fetch(`/api/credits?userId=${uid}`);
45+
if (res.ok) setCredits(await res.json());
46+
} catch {}
47+
setLoading(false);
48+
}
49+
50+
const planTiers = [
51+
{ name: 'Free', price: '$0', credits: '500/mo', current: true },
52+
{ name: 'Pro', price: '$20/mo', credits: '50,000/mo', highlight: true },
53+
{ name: 'Enterprise', price: 'Custom', credits: 'Unlimited' },
54+
];
55+
56+
if (loading) {
57+
return (
58+
<div className="min-h-screen bg-background flex items-center justify-center">
59+
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
60+
</div>
61+
);
62+
}
63+
64+
return (
65+
<div className="flex flex-col min-h-screen bg-background text-foreground text-sm">
66+
<header className="sticky top-0 z-50 flex items-center justify-between h-14 px-6 border-b border-border bg-background/80 backdrop-blur-md">
67+
<div className="flex items-center gap-4">
68+
<Link href={`/${locale}/account`} className="p-1.5 rounded-lg hover:bg-muted transition-colors">
69+
<ArrowLeft size={18} />
70+
</Link>
71+
<h1 className="font-semibold">Billing & Credits</h1>
72+
</div>
73+
<ThemeToggle />
74+
</header>
75+
76+
<div className="max-w-2xl mx-auto w-full py-8 px-4 space-y-6">
77+
{/* Credit Balance Card */}
78+
<div className="rounded-xl border border-border bg-gradient-to-br from-primary/5 to-primary/10 p-6">
79+
<div className="flex items-center justify-between mb-4">
80+
<div className="flex items-center gap-3">
81+
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center">
82+
<Zap size={18} className="text-primary-foreground" />
83+
</div>
84+
<div>
85+
<p className="text-[10px] font-semibold tracking-widest text-muted-foreground uppercase">AI Credits</p>
86+
<p className="text-3xl font-extrabold text-primary">
87+
{credits ? credits.balance.toLocaleString() : '0'}
88+
</p>
89+
</div>
90+
</div>
91+
<button
92+
onClick={() => userId && fetchCredits(userId)}
93+
className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
94+
>
95+
<RefreshCw size={14} />
96+
</button>
97+
</div>
98+
<div className="flex items-center gap-6 text-xs text-muted-foreground">
99+
<span className="flex items-center gap-1"><TrendingUp size={12} /> Lifetime: {credits?.lifetime.toLocaleString() || '0'} credits used</span>
100+
</div>
101+
<div className="mt-4 flex gap-3">
102+
<button className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:bg-primary/90 transition-colors flex items-center gap-1.5">
103+
<CreditCard size={13} /> Buy Credits
104+
</button>
105+
<button className="px-4 py-2 rounded-lg border border-border text-xs font-medium hover:bg-muted transition-colors">
106+
Exchange UET Coin
107+
</button>
108+
</div>
109+
</div>
110+
111+
{/* Credit Costs */}
112+
<div className="rounded-xl border border-border p-5">
113+
<h2 className="font-semibold mb-4">Credit Costs</h2>
114+
<div className="grid grid-cols-2 gap-3">
115+
{[
116+
{ action: 'Chat message (standard)', cost: '1-5' },
117+
{ action: 'Chat message (reasoning)', cost: '5-20' },
118+
{ action: 'Document ingestion (per page)', cost: '2' },
119+
{ action: 'OmegaSearch query', cost: '1' },
120+
{ action: 'Image generation', cost: '10' },
121+
{ action: 'Voice transcription (per min)', cost: '5' },
122+
].map(item => (
123+
<div key={item.action} className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
124+
<span className="text-xs">{item.action}</span>
125+
<span className="text-xs font-semibold text-primary">{item.cost} cr</span>
126+
</div>
127+
))}
128+
</div>
129+
</div>
130+
131+
{/* Plan Comparison */}
132+
<div className="rounded-xl border border-border p-5">
133+
<h2 className="font-semibold mb-4">Plans</h2>
134+
<div className="grid grid-cols-3 gap-3">
135+
{planTiers.map(tier => (
136+
<div
137+
key={tier.name}
138+
className={`p-4 rounded-xl border text-center ${
139+
tier.highlight ? 'border-primary bg-primary/5' : 'border-border'
140+
}`}
141+
>
142+
<h3 className="font-semibold mb-1">{tier.name}</h3>
143+
<p className="text-xl font-bold mb-1">{tier.price}</p>
144+
<p className="text-xs text-muted-foreground mb-3">{tier.credits}</p>
145+
<Link
146+
href={`/${locale}/pricing`}
147+
className={`block w-full py-1.5 rounded-lg text-xs font-medium transition-colors ${
148+
tier.current
149+
? 'bg-muted text-muted-foreground'
150+
: tier.highlight
151+
? 'bg-primary text-primary-foreground hover:bg-primary/90'
152+
: 'border border-border hover:bg-muted'
153+
}`}
154+
>
155+
{tier.current ? 'Current' : tier.highlight ? 'Upgrade' : 'Contact'}
156+
</Link>
157+
</div>
158+
))}
159+
</div>
160+
</div>
161+
162+
{/* Transaction History */}
163+
<div className="rounded-xl border border-border p-5">
164+
<h2 className="font-semibold mb-4">Credit History</h2>
165+
{!credits || credits.transactions.length === 0 ? (
166+
<p className="text-center text-muted-foreground py-8 text-xs">No transactions yet</p>
167+
) : (
168+
<div className="space-y-1">
169+
{credits.transactions.map(tx => {
170+
const isPositive = tx.amount > 0;
171+
return (
172+
<div key={tx.id} className="flex items-center gap-3 p-3 rounded-lg hover:bg-muted/30 transition-colors">
173+
<div className={`w-7 h-7 rounded-full flex items-center justify-center ${
174+
isPositive ? 'bg-green-500/10 text-green-600' : 'bg-red-500/10 text-red-500'
175+
}`}>
176+
{isPositive ? <ArrowDownLeft size={13} /> : <ArrowUpRight size={13} />}
177+
</div>
178+
<div className="flex-1 min-w-0">
179+
<p className="text-xs font-medium">{tx.type.replace(/_/g, ' ')}</p>
180+
{tx.description && <p className="text-[10px] text-muted-foreground truncate">{tx.description}</p>}
181+
</div>
182+
<div className="text-right">
183+
<p className={`text-sm font-bold ${isPositive ? 'text-green-600' : 'text-red-500'}`}>
184+
{isPositive ? '+' : ''}{tx.amount.toLocaleString()}
185+
</p>
186+
<p className="text-[10px] text-muted-foreground">{new Date(tx.createdAt).toLocaleDateString()}</p>
187+
</div>
188+
</div>
189+
);
190+
})}
191+
</div>
192+
)}
193+
</div>
194+
</div>
195+
</div>
196+
);
197+
}

0 commit comments

Comments
 (0)