Skip to content

Commit ef16a8e

Browse files
committed
feat: Usage page -- plan info, resource bars, feature list (OPE-103)
New page at /dashboard/usage showing: - Plan card with tier badge (free/pro/enterprise, color-coded) - Resource usage bars: repos (with progress bar), files/repo, functions/repo - Bar colors: green <70%, amber 70-90%, red >90% - Feature grid: Semantic Search, DNA, MCP Access, Priority Indexing - Locked features show Pro badge + lock icon - Loading skeleton while data fetches - Uses useUserUsage hook (GET /users/usage, React Query) Route: /dashboard/usage in Dashboard.tsx Sidebar: BarChart3 icon + Usage nav link 210 lines, 4 sub-components (PlanCard, UsageBar, FeatureItem, UsageSkeleton). All shadcn: Card, Badge, Separator, Skeleton. No custom UI.
1 parent 2b19e67 commit ef16a8e

3 files changed

Lines changed: 214 additions & 0 deletions

File tree

frontend/src/components/Dashboard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { Routes, Route, Navigate } from 'react-router-dom'
22
import { DashboardLayout } from './dashboard/DashboardLayout'
33
import { DashboardHome } from './dashboard/DashboardHome'
44
import { SettingsPage } from '../pages/SettingsPage'
5+
import { UsagePage } from '../pages/UsagePage'
56

67
export function Dashboard() {
78
return (
89
<DashboardLayout>
910
<Routes>
1011
<Route index element={<DashboardHome />} />
12+
<Route path="usage" element={<UsagePage />} />
1113
<Route path="settings" element={<SettingsPage />} />
1214
<Route path="*" element={<Navigate to="/dashboard" replace />} />
1315
</Routes>

frontend/src/components/dashboard/Sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Link, useLocation } from 'react-router-dom'
22
import {
33
FolderGit2,
4+
BarChart3,
45
BookOpen,
56
ChevronLeft,
67
ChevronRight,
@@ -24,6 +25,7 @@ interface NavItem {
2425

2526
const mainNavItems: NavItem[] = [
2627
{ name: 'Repositories', href: '/dashboard', icon: <FolderGit2 className="w-5 h-5" /> },
28+
{ name: 'Usage', href: '/dashboard/usage', icon: <BarChart3 className="w-5 h-5" /> },
2729
]
2830

2931
const bottomNavItems: NavItem[] = [

frontend/src/pages/UsagePage.tsx

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* UsagePage -- plan info, resource usage, and feature availability.
3+
*
4+
* Fetches from GET /users/usage. Shows tier, repo usage bars,
5+
* function/file limits, and which features are available on the
6+
* user's current plan.
7+
*/
8+
9+
import { useAuth } from '@/contexts/AuthContext'
10+
import { useUserUsage } from '@/hooks/useCachedQuery'
11+
import { BarChart3, Package, FunctionSquare, Files, Zap, Search, Server, Sparkles, Lock } from 'lucide-react'
12+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
13+
import { Badge } from '@/components/ui/badge'
14+
import { Separator } from '@/components/ui/separator'
15+
import { Skeleton } from '@/components/ui/Skeleton'
16+
import { cn } from '@/lib/utils'
17+
18+
const TIER_COLORS: Record<string, string> = {
19+
free: 'bg-muted text-muted-foreground',
20+
pro: 'bg-primary/10 text-primary border-primary/20',
21+
enterprise: 'bg-amber-500/10 text-amber-500 border-amber-500/20',
22+
}
23+
24+
export function UsagePage() {
25+
const { session } = useAuth()
26+
const { data: usage, isLoading } = useUserUsage(session?.access_token, session?.user?.id)
27+
28+
if (isLoading) return <UsageSkeleton />
29+
30+
if (!usage) {
31+
return (
32+
<div className="flex items-center justify-center min-h-[300px] text-muted-foreground">
33+
Failed to load usage data.
34+
</div>
35+
)
36+
}
37+
38+
const tier = usage.tier || 'free'
39+
const repos = usage.repositories || { current: 0, limit: 3 }
40+
const limits = usage.limits || { max_files_per_repo: 500, max_functions_per_repo: 2000 }
41+
const features = usage.features || { priority_indexing: false, mcp_access: true }
42+
43+
return (
44+
<div className="space-y-6 max-w-3xl">
45+
<div className="flex items-center gap-3">
46+
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
47+
<BarChart3 className="w-5 h-5 text-primary" />
48+
</div>
49+
<div>
50+
<h1 className="text-2xl font-bold">Usage</h1>
51+
<p className="text-sm text-muted-foreground">Plan details and resource limits</p>
52+
</div>
53+
</div>
54+
55+
<PlanCard tier={tier} />
56+
57+
<Card>
58+
<CardHeader className="pb-3">
59+
<CardTitle className="text-base">Resource Usage</CardTitle>
60+
</CardHeader>
61+
<CardContent className="space-y-5">
62+
<UsageBar
63+
icon={<Package className="w-4 h-4" />}
64+
label="Repositories"
65+
current={repos.current}
66+
limit={repos.limit}
67+
/>
68+
<Separator />
69+
<UsageBar
70+
icon={<Files className="w-4 h-4" />}
71+
label="Files per repository"
72+
current={null}
73+
limit={limits.max_files_per_repo}
74+
/>
75+
<Separator />
76+
<UsageBar
77+
icon={<FunctionSquare className="w-4 h-4" />}
78+
label="Functions per repository"
79+
current={null}
80+
limit={limits.max_functions_per_repo}
81+
/>
82+
</CardContent>
83+
</Card>
84+
85+
<Card>
86+
<CardHeader className="pb-3">
87+
<CardTitle className="text-base">Features</CardTitle>
88+
</CardHeader>
89+
<CardContent>
90+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
91+
<FeatureItem icon={<Search className="w-4 h-4" />} label="Semantic Code Search" enabled />
92+
<FeatureItem icon={<Sparkles className="w-4 h-4" />} label="Codebase DNA" enabled />
93+
<FeatureItem icon={<Server className="w-4 h-4" />} label="MCP Server Access" enabled={features.mcp_access} />
94+
<FeatureItem icon={<Zap className="w-4 h-4" />} label="Priority Indexing" enabled={features.priority_indexing} />
95+
</div>
96+
</CardContent>
97+
</Card>
98+
</div>
99+
)
100+
}
101+
102+
103+
function PlanCard({ tier }: { tier: string }) {
104+
return (
105+
<Card>
106+
<CardContent className="flex items-center justify-between py-5">
107+
<div className="flex items-center gap-3">
108+
<span className="text-sm text-muted-foreground">Current plan</span>
109+
<Badge
110+
variant="outline"
111+
className={cn('capitalize text-sm px-3 py-1', TIER_COLORS[tier])}
112+
>
113+
{tier}
114+
</Badge>
115+
</div>
116+
{tier === 'free' && (
117+
<span className="text-xs text-muted-foreground">
118+
Upgrade coming soon
119+
</span>
120+
)}
121+
</CardContent>
122+
</Card>
123+
)
124+
}
125+
126+
127+
function UsageBar({
128+
icon,
129+
label,
130+
current,
131+
limit,
132+
}: {
133+
icon: React.ReactNode
134+
label: string
135+
current: number | null
136+
limit: number | null
137+
}) {
138+
const hasBar = current !== null && limit !== null && limit > 0
139+
const pct = hasBar ? Math.min((current / limit!) * 100, 100) : 0
140+
const barColor = pct > 90 ? 'bg-destructive' : pct > 70 ? 'bg-amber-500' : 'bg-emerald-500'
141+
142+
return (
143+
<div className="space-y-2">
144+
<div className="flex items-center justify-between">
145+
<div className="flex items-center gap-2 text-sm">
146+
<span className="text-muted-foreground">{icon}</span>
147+
{label}
148+
</div>
149+
<span className="text-sm tabular-nums text-muted-foreground">
150+
{current !== null ? `${current.toLocaleString()} / ` : 'up to '}
151+
{limit !== null ? limit.toLocaleString() : 'unlimited'}
152+
</span>
153+
</div>
154+
{hasBar && (
155+
<div className="h-2 rounded-full bg-muted overflow-hidden">
156+
<div
157+
className={cn('h-full rounded-full transition-all duration-500 ease-out', barColor)}
158+
style={{ width: `${Math.max(pct, 2)}%` }}
159+
/>
160+
</div>
161+
)}
162+
</div>
163+
)
164+
}
165+
166+
167+
function FeatureItem({
168+
icon,
169+
label,
170+
enabled,
171+
}: {
172+
icon: React.ReactNode
173+
label: string
174+
enabled: boolean
175+
}) {
176+
return (
177+
<div className={cn(
178+
'flex items-center gap-2.5 rounded-lg border px-3 py-2.5 text-sm',
179+
enabled
180+
? 'border-border text-foreground'
181+
: 'border-border/50 text-muted-foreground opacity-60',
182+
)}>
183+
<span className={enabled ? 'text-primary' : 'text-muted-foreground'}>
184+
{enabled ? icon : <Lock className="w-4 h-4" />}
185+
</span>
186+
{label}
187+
{!enabled && (
188+
<Badge variant="outline" className="ml-auto text-[10px] px-1.5 py-0">Pro</Badge>
189+
)}
190+
</div>
191+
)
192+
}
193+
194+
195+
function UsageSkeleton() {
196+
return (
197+
<div className="space-y-6 max-w-3xl">
198+
<div className="flex items-center gap-3">
199+
<Skeleton className="w-10 h-10 rounded-xl" />
200+
<div className="space-y-2">
201+
<Skeleton className="h-6 w-24" />
202+
<Skeleton className="h-4 w-48" />
203+
</div>
204+
</div>
205+
<Skeleton className="h-20 rounded-lg" />
206+
<Skeleton className="h-48 rounded-lg" />
207+
<Skeleton className="h-32 rounded-lg" />
208+
</div>
209+
)
210+
}

0 commit comments

Comments
 (0)