22 * UsagePage -- plan info, resource usage, and feature availability.
33 *
44 * Fetches from GET /users/usage. Shows tier, repo usage,
5- * limits, and features in a compact two-column layout .
5+ * limits, and features. Premium design matching API Keys page .
66 */
77
88import { useAuth } from '@/contexts/AuthContext'
99import { useUserUsage } from '@/hooks/useCachedQuery'
1010import {
11- BarChart3 , Package , FunctionSquare , Files , Zap , Search ,
12- Server , Sparkles , Lock , ArrowRight , TrendingUp ,
11+ Package , FunctionSquare , Files , Zap , Search ,
12+ Server , Sparkles , Lock , ArrowRight , TrendingUp , Check ,
1313} from 'lucide-react'
14- import { Card , CardContent } from '@/components/ui/card'
1514import { Button } from '@/components/ui/button'
1615import { Badge } from '@/components/ui/badge'
17- import { Skeleton } from '@/components/ui/Skeleton'
1816import { cn } from '@/lib/utils'
1917
20- const TIER_COLORS : Record < string , string > = {
21- free : 'bg-muted text-muted-foreground' ,
22- pro : 'bg-primary/10 text-primary border-primary/20' ,
23- enterprise : 'bg-amber-500/10 text-amber-500 border-amber-500/20' ,
18+ const TIER_STYLES : Record < string , { bg : string ; text : string ; accent : string ; label : string } > = {
19+ enterprise : {
20+ bg : 'bg-amber-500/8 border-amber-500/15' ,
21+ text : 'text-amber-400' ,
22+ accent : 'bg-amber-400' ,
23+ label : 'Enterprise' ,
24+ } ,
25+ pro : {
26+ bg : 'bg-indigo-500/8 border-indigo-500/15' ,
27+ text : 'text-indigo-400' ,
28+ accent : 'bg-indigo-400' ,
29+ label : 'Pro' ,
30+ } ,
31+ free : {
32+ bg : 'bg-zinc-500/8 border-zinc-500/15' ,
33+ text : 'text-zinc-400' ,
34+ accent : 'bg-zinc-500' ,
35+ label : 'Free' ,
36+ } ,
2437}
2538
2639export function UsagePage ( ) {
@@ -37,138 +50,166 @@ export function UsagePage() {
3750 }
3851
3952 const tier = usage . tier || 'free'
53+ const tierStyle = TIER_STYLES [ tier ] || TIER_STYLES . free
4054 const repos = usage . repositories || { current : 0 , limit : 3 }
4155 const limits = usage . limits || { max_files_per_repo : 500 , max_functions_per_repo : 2000 }
4256 const features = usage . features || { priority_indexing : false , mcp_access : true }
4357 const isFree = tier === 'free'
58+ const repoPct = repos . limit ? ( repos . current / repos . limit ) * 100 : 0
4459
4560 return (
46- < div className = "space-y-4 max-w-4xl" >
47- { /* Header */ }
48- < div className = "flex items-center gap-3" >
49- < div className = "w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center" >
50- < BarChart3 className = "w-5 h-5 text-primary" />
51- </ div >
61+ < div className = "space-y-8" >
62+ { /* Page header */ }
63+ < div className = "flex items-start justify-between" >
5264 < div >
53- < div className = "flex items-center gap-2" >
54- < h1 className = "text-2xl font-bold" > Usage</ h1 >
55- < Badge variant = "outline" className = { cn ( 'capitalize' , TIER_COLORS [ tier ] ) } >
56- { tier }
65+ < div className = "flex items-center gap-3" >
66+ < h1 className = "text-2xl font-semibold tracking-tight text-foreground" >
67+ Usage
68+ </ h1 >
69+ < Badge
70+ variant = "outline"
71+ className = { cn (
72+ 'text-[10px] uppercase tracking-wider font-medium border' ,
73+ tierStyle . bg , tierStyle . text
74+ ) }
75+ >
76+ { tierStyle . label }
5777 </ Badge >
5878 </ div >
59- < p className = "text-sm text-muted-foreground" > Plan details and resource limits</ p >
79+ < p className = "text-sm text-muted-foreground mt-1" >
80+ Plan details, resource limits, and feature availability.
81+ </ p >
6082 </ div >
61- </ div >
62-
63- { /* Upgrade CTA for free users */ }
64- { isFree && (
65- < div className = "flex items-center justify-between rounded-lg bg-primary/5 border border-primary/10 px-4 py-3" >
66- < div >
67- < p className = "text-sm font-medium" > Unlock higher limits and priority indexing</ p >
68- < p className = "text-xs text-muted-foreground mt-0.5" >
69- Pro: 5 repos, 100K functions/repo, Cohere reranking
70- </ p >
71- </ div >
83+ { isFree && (
7284 < Button
7385 size = "sm"
74- className = "ml-4 shrink-0 "
86+ className = "h-9 "
7587 onClick = { ( ) => window . open ( 'https://opencodeintel.com/#pricing' , '_blank' ) }
7688 >
77- Upgrade to Pro
78- < ArrowRight className = "w-3.5 h-3.5 ml-1" />
89+ Upgrade
90+ < ArrowRight className = "w-3.5 h-3.5 ml-1.5 " />
7991 </ Button >
92+ ) }
93+ </ div >
94+
95+ { /* Upgrade banner for free tier */ }
96+ { isFree && (
97+ < div className = "rounded-lg border border-indigo-500/15 bg-indigo-500/5 px-5 py-4" >
98+ < p className = "text-sm font-medium text-foreground" >
99+ Unlock higher limits and priority indexing
100+ </ p >
101+ < p className = "text-xs text-muted-foreground mt-1 leading-relaxed" >
102+ Pro gives you 5 repos, 100K functions per repo, Cohere reranking, and priority indexing.
103+ </ p >
80104 </ div >
81105 ) }
82106
83- { /* Two-column: Usage + Features */ }
84- < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
85- { /* Left: Resource Usage */ }
86- < Card >
87- < CardContent className = "pt-5 space-y-4" >
88- < p className = "text-sm font-medium text-muted-foreground" > Resource Limits</ p >
89- < UsageRow
90- icon = { < Package className = "w-4 h-4" /> }
91- label = "Repositories"
92- value = { `${ repos . current } / ${ repos . limit ?? 'unlimited' } ` }
93- pct = { repos . limit ? ( repos . current / repos . limit ) * 100 : 0 }
94- showBar
107+ { /* Resource usage cards */ }
108+ < div >
109+ < p className = "text-xs font-medium uppercase tracking-wider text-muted-foreground/60 mb-3" >
110+ Resources
111+ </ p >
112+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-3" >
113+ { /* Repositories */ }
114+ < div className = "rounded-lg border border-border/60 bg-card/40 px-5 py-4" >
115+ < div className = "flex items-center justify-between mb-3" >
116+ < div className = "flex items-center gap-2 text-sm text-muted-foreground" >
117+ < Package className = "w-4 h-4" />
118+ Repositories
119+ </ div >
120+ < span className = "text-xs tabular-nums text-muted-foreground/60" >
121+ { repos . current } / { repos . limit ?? '---' }
122+ </ span >
123+ </ div >
124+ < div className = "text-2xl font-semibold tabular-nums text-foreground mb-2" >
125+ { repos . current }
126+ </ div >
127+ { repos . limit && (
128+ < div className = "h-1.5 rounded-full bg-muted/60 overflow-hidden" >
129+ < div
130+ className = { cn (
131+ 'h-full rounded-full transition-all duration-700' ,
132+ repoPct > 90 ? 'bg-red-500' : repoPct > 70 ? 'bg-amber-500' : 'bg-emerald-500'
133+ ) }
134+ style = { { width : `${ repoPct > 0 ? Math . max ( repoPct , 4 ) : 0 } %` } }
135+ />
136+ </ div >
137+ ) }
138+ </ div >
139+
140+ { /* Files per repo */ }
141+ < div className = "rounded-lg border border-border/60 bg-card/40 px-5 py-4" >
142+ < div className = "flex items-center gap-2 text-sm text-muted-foreground mb-3" >
143+ < Files className = "w-4 h-4" />
144+ Files per repo
145+ </ div >
146+ < div className = "text-2xl font-semibold tabular-nums text-foreground" >
147+ { limits . max_files_per_repo . toLocaleString ( ) }
148+ </ div >
149+ < p className = "text-[11px] text-muted-foreground/50 mt-1" > maximum</ p >
150+ </ div >
151+
152+ { /* Functions per repo */ }
153+ < div className = "rounded-lg border border-border/60 bg-card/40 px-5 py-4" >
154+ < div className = "flex items-center gap-2 text-sm text-muted-foreground mb-3" >
155+ < FunctionSquare className = "w-4 h-4" />
156+ Functions per repo
157+ </ div >
158+ < div className = "text-2xl font-semibold tabular-nums text-foreground" >
159+ { limits . max_functions_per_repo . toLocaleString ( ) }
160+ </ div >
161+ < p className = "text-[11px] text-muted-foreground/50 mt-1" > maximum</ p >
162+ </ div >
163+ </ div >
164+ </ div >
165+
166+ { /* Features */ }
167+ < div >
168+ < p className = "text-xs font-medium uppercase tracking-wider text-muted-foreground/60 mb-3" >
169+ Features
170+ </ p >
171+ < div className = "rounded-lg border border-border/60 bg-card/40 overflow-hidden" >
172+ < div className = "divide-y divide-border/40" >
173+ < FeatureRow
174+ icon = { < Search className = "w-4 h-4" /> }
175+ label = "Semantic Code Search"
176+ description = "Find code by meaning, not just keywords"
177+ enabled
95178 />
96- < UsageRow
97- icon = { < Files className = "w-4 h-4" /> }
98- label = "Files / repo"
99- value = { `up to ${ limits . max_files_per_repo . toLocaleString ( ) } ` }
179+ < FeatureRow
180+ icon = { < Sparkles className = "w-4 h-4" /> }
181+ label = "Codebase DNA"
182+ description = "Extract architectural patterns and conventions"
183+ enabled
100184 />
101- < UsageRow
102- icon = { < FunctionSquare className = "w-4 h-4" /> }
103- label = "Functions / repo"
104- value = { `up to ${ limits . max_functions_per_repo . toLocaleString ( ) } ` }
185+ < FeatureRow
186+ icon = { < Server className = "w-4 h-4" /> }
187+ label = "MCP Server Access"
188+ description = "Connect Claude Desktop, Claude Code, and Cursor"
189+ enabled = { features . mcp_access }
105190 />
106- </ CardContent >
107- </ Card >
108-
109- { /* Right: Features */ }
110- < Card >
111- < CardContent className = "pt-5 space-y-3" >
112- < p className = "text-sm font-medium text-muted-foreground" > Features</ p >
113- < FeatureRow icon = { < Search className = "w-4 h-4" /> } label = "Semantic Code Search" enabled />
114- < FeatureRow icon = { < Sparkles className = "w-4 h-4" /> } label = "Codebase DNA" enabled />
115- < FeatureRow icon = { < Server className = "w-4 h-4" /> } label = "MCP Server Access" enabled = { features . mcp_access } />
116- < FeatureRow icon = { < Zap className = "w-4 h-4" /> } label = "Priority Indexing" enabled = { features . priority_indexing } />
117- </ CardContent >
118- </ Card >
191+ < FeatureRow
192+ icon = { < Zap className = "w-4 h-4" /> }
193+ label = "Priority Indexing"
194+ description = "Skip the queue on indexing and re-indexing"
195+ enabled = { features . priority_indexing }
196+ />
197+ </ div >
198+ </ div >
119199 </ div >
120200
121201 { /* Cost tracking placeholder */ }
122- < Card className = "border-dashed" >
123- < CardContent className = "py-5 flex items-center gap-4" >
124- < div className = "w-9 h-9 rounded-full bg-muted flex items-center justify-center shrink-0" >
125- < TrendingUp className = "w-4 h-4 text-muted-foreground" />
126- </ div >
127- < div >
128- < p className = "text-sm font-medium" > API Cost Tracking</ p >
129- < p className = "text-xs text-muted-foreground" >
130- Token usage, cost breakdown by model, and monthly spend tracking -- coming soon.
131- </ p >
132- </ div >
133- </ CardContent >
134- </ Card >
135- </ div >
136- )
137- }
138-
139-
140- function UsageRow ( {
141- icon,
142- label,
143- value,
144- pct,
145- showBar,
146- } : {
147- icon : React . ReactNode
148- label : string
149- value : string
150- pct ?: number
151- showBar ?: boolean
152- } ) {
153- const barColor = ( pct ?? 0 ) > 90 ? 'bg-destructive' : ( pct ?? 0 ) > 70 ? 'bg-amber-500' : 'bg-emerald-500'
154-
155- return (
156- < div className = "space-y-1.5" >
157- < div className = "flex items-center justify-between text-sm" >
158- < div className = "flex items-center gap-2" >
159- < span className = "text-muted-foreground" > { icon } </ span >
160- { label }
202+ < div className = "rounded-lg border border-dashed border-border/40 bg-card/20 px-5 py-4 flex items-center gap-4" >
203+ < div className = "w-9 h-9 rounded-full bg-muted/30 flex items-center justify-center shrink-0" >
204+ < TrendingUp className = "w-4 h-4 text-muted-foreground/50" />
161205 </ div >
162- < span className = "tabular-nums text-muted-foreground" > { value } </ span >
163- </ div >
164- { showBar && pct !== undefined && (
165- < div className = "h-1.5 rounded-full bg-muted overflow-hidden" >
166- < div
167- className = { cn ( 'h-full rounded-full transition-all duration-500' , barColor ) }
168- style = { { width : `${ pct > 0 ? Math . max ( pct , 2 ) : 0 } %` } }
169- />
206+ < div >
207+ < p className = "text-sm font-medium text-muted-foreground" > API Cost Tracking</ p >
208+ < p className = "text-[11px] text-muted-foreground/50 mt-0.5" >
209+ Token usage, cost breakdown by model, and monthly spend tracking -- coming soon.
210+ </ p >
170211 </ div >
171- ) }
212+ </ div >
172213 </ div >
173214 )
174215}
@@ -177,23 +218,40 @@ function UsageRow({
177218function FeatureRow ( {
178219 icon,
179220 label,
221+ description,
180222 enabled,
181223} : {
182224 icon : React . ReactNode
183225 label : string
226+ description : string
184227 enabled : boolean
185228} ) {
186229 return (
187230 < div className = { cn (
188- 'flex items-center gap-2.5 text-sm py-1 ' ,
189- enabled ? 'text-foreground' : 'text-muted-foreground opacity-50 ',
231+ 'flex items-center gap-4 px-5 py-3.5 ' ,
232+ ! enabled && ' opacity-40 ',
190233 ) } >
191- < span className = { enabled ? 'text-primary' : '' } >
234+ < div className = { cn (
235+ 'w-8 h-8 rounded-lg flex items-center justify-center shrink-0' ,
236+ enabled
237+ ? 'bg-primary/10 text-primary'
238+ : 'bg-muted/50 text-muted-foreground'
239+ ) } >
192240 { enabled ? icon : < Lock className = "w-4 h-4" /> }
193- </ span >
194- < span className = "flex-1" > { label } </ span >
195- { ! enabled && (
196- < Badge variant = "outline" className = "text-[10px] px-1.5 py-0" > Pro</ Badge >
241+ </ div >
242+ < div className = "flex-1 min-w-0" >
243+ < p className = "text-sm font-medium text-foreground" > { label } </ p >
244+ < p className = "text-[11px] text-muted-foreground/60 mt-0.5" > { description } </ p >
245+ </ div >
246+ { enabled ? (
247+ < Check className = "w-4 h-4 text-emerald-400 shrink-0" />
248+ ) : (
249+ < Badge
250+ variant = "outline"
251+ className = "text-[10px] uppercase tracking-wider border-indigo-500/15 bg-indigo-500/8 text-indigo-400 shrink-0"
252+ >
253+ Pro
254+ </ Badge >
197255 ) }
198256 </ div >
199257 )
@@ -202,18 +260,17 @@ function FeatureRow({
202260
203261function UsageSkeleton ( ) {
204262 return (
205- < div className = "space-y-4 max-w-4xl" >
206- < div className = "flex items-center gap-3" >
207- < Skeleton className = "w-10 h-10 rounded-xl" />
208- < div className = "space-y-2" >
209- < Skeleton className = "h-6 w-32" />
210- < Skeleton className = "h-4 w-48" />
211- </ div >
263+ < div className = "space-y-8" >
264+ < div className = "space-y-2" >
265+ < div className = "h-7 w-48 rounded bg-muted/50 animate-pulse" />
266+ < div className = "h-4 w-72 rounded bg-muted/30 animate-pulse" />
212267 </ div >
213- < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
214- < Skeleton className = "h-40 rounded-lg" />
215- < Skeleton className = "h-40 rounded-lg" />
268+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-3" >
269+ { [ 1 , 2 , 3 ] . map ( ( i ) => (
270+ < div key = { i } className = "h-28 rounded-lg bg-muted/20 animate-pulse" />
271+ ) ) }
216272 </ div >
273+ < div className = "h-52 rounded-lg bg-muted/20 animate-pulse" />
217274 </ div >
218275 )
219276}
0 commit comments