costhawk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/STRATEGIC_PLAN_2025-12-01.md +934 -0
  3. package/costcanary/.claude/settings.local.json +9 -0
  4. package/costcanary/.env.production.template +38 -0
  5. package/costcanary/.eslintrc.json +22 -0
  6. package/costcanary/.nvmrc +1 -0
  7. package/costcanary/.prettierignore +11 -0
  8. package/costcanary/.prettierrc.json +12 -0
  9. package/costcanary/ADMIN_SETUP.md +68 -0
  10. package/costcanary/CLAUDE.md +228 -0
  11. package/costcanary/CLERK_SETUP.md +69 -0
  12. package/costcanary/DATABASE_SETUP.md +136 -0
  13. package/costcanary/DEMO_CHECKLIST.md +62 -0
  14. package/costcanary/DEPLOYMENT.md +31 -0
  15. package/costcanary/PRODUCTION_RECOVERY.md +109 -0
  16. package/costcanary/README.md +247 -0
  17. package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
  18. package/costcanary/TESTING_ADMIN.md +92 -0
  19. package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
  20. package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
  21. package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
  22. package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
  23. package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
  24. package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
  25. package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
  26. package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
  27. package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
  28. package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
  29. package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
  30. package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
  31. package/costcanary/app/(marketing)/page.tsx +90 -0
  32. package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
  33. package/costcanary/app/admin/pricing-status/page.tsx +338 -0
  34. package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
  35. package/costcanary/app/api/admin/debug/route.ts +44 -0
  36. package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
  37. package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
  38. package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
  39. package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
  40. package/costcanary/app/api/admin/whoami/route.ts +44 -0
  41. package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
  42. package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
  43. package/costcanary/app/api/debug-status/route.ts +9 -0
  44. package/costcanary/app/api/debug-version/route.ts +12 -0
  45. package/costcanary/app/api/health/route.ts +14 -0
  46. package/costcanary/app/api/health-simple/route.ts +18 -0
  47. package/costcanary/app/api/keys/route.ts +162 -0
  48. package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
  49. package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
  50. package/costcanary/app/api/keys/wrapped/route.ts +241 -0
  51. package/costcanary/app/api/optimizer/preview/route.ts +147 -0
  52. package/costcanary/app/api/optimizer/route.ts +118 -0
  53. package/costcanary/app/api/pricing/models/route.ts +102 -0
  54. package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
  55. package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
  56. package/costcanary/app/api/proxy/google/route.ts +395 -0
  57. package/costcanary/app/api/proxy/openai/route.ts +529 -0
  58. package/costcanary/app/api/simple-test/route.ts +7 -0
  59. package/costcanary/app/api/stripe/checkout/route.ts +201 -0
  60. package/costcanary/app/api/stripe/webhook/route.ts +392 -0
  61. package/costcanary/app/api/test-connection/route.ts +209 -0
  62. package/costcanary/app/api/test-proxy/route.ts +7 -0
  63. package/costcanary/app/api/test-simple/route.ts +20 -0
  64. package/costcanary/app/api/usage/current/route.ts +112 -0
  65. package/costcanary/app/api/usage/stats/route.ts +129 -0
  66. package/costcanary/app/api/usage/stream/route.ts +113 -0
  67. package/costcanary/app/api/usage/summary/route.ts +67 -0
  68. package/costcanary/app/api/usage/trend/route.ts +119 -0
  69. package/costcanary/app/api/ws/route.ts +23 -0
  70. package/costcanary/app/globals.css +280 -0
  71. package/costcanary/app/layout.tsx +87 -0
  72. package/costcanary/components/Header.tsx +85 -0
  73. package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
  74. package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
  75. package/costcanary/components/landing/DashboardPreview.tsx +222 -0
  76. package/costcanary/components/landing/Features.tsx +238 -0
  77. package/costcanary/components/landing/Footer.tsx +83 -0
  78. package/costcanary/components/landing/Hero.tsx +193 -0
  79. package/costcanary/components/landing/Pricing.tsx +250 -0
  80. package/costcanary/components/landing/Testimonials.tsx +248 -0
  81. package/costcanary/components/theme-provider.tsx +8 -0
  82. package/costcanary/components/ui/alert.tsx +59 -0
  83. package/costcanary/components/ui/badge.tsx +36 -0
  84. package/costcanary/components/ui/button.tsx +56 -0
  85. package/costcanary/components/ui/card.tsx +79 -0
  86. package/costcanary/components/ui/dialog.tsx +122 -0
  87. package/costcanary/components/ui/input.tsx +22 -0
  88. package/costcanary/components/ui/label.tsx +26 -0
  89. package/costcanary/components/ui/progress.tsx +28 -0
  90. package/costcanary/components/ui/select.tsx +160 -0
  91. package/costcanary/components/ui/separator.tsx +31 -0
  92. package/costcanary/components/ui/switch.tsx +29 -0
  93. package/costcanary/components/ui/tabs.tsx +55 -0
  94. package/costcanary/components/ui/toast.tsx +127 -0
  95. package/costcanary/components/ui/toaster.tsx +35 -0
  96. package/costcanary/components/ui/use-toast.ts +189 -0
  97. package/costcanary/components.json +17 -0
  98. package/costcanary/debug-wrapped-keys.md +117 -0
  99. package/costcanary/fix-console.sh +30 -0
  100. package/costcanary/lib/admin-auth.ts +226 -0
  101. package/costcanary/lib/admin-security.ts +124 -0
  102. package/costcanary/lib/audit-events.ts +62 -0
  103. package/costcanary/lib/audit.ts +158 -0
  104. package/costcanary/lib/chart-colors.ts +152 -0
  105. package/costcanary/lib/cost-calculator.ts +212 -0
  106. package/costcanary/lib/db-utils.ts +325 -0
  107. package/costcanary/lib/db.ts +14 -0
  108. package/costcanary/lib/encryption.ts +120 -0
  109. package/costcanary/lib/kms.ts +358 -0
  110. package/costcanary/lib/model-alias.ts +180 -0
  111. package/costcanary/lib/pricing.ts +292 -0
  112. package/costcanary/lib/prisma.ts +52 -0
  113. package/costcanary/lib/railway-db.ts +157 -0
  114. package/costcanary/lib/sse-parser.ts +283 -0
  115. package/costcanary/lib/stripe-client.ts +81 -0
  116. package/costcanary/lib/stripe-server.ts +52 -0
  117. package/costcanary/lib/tokens.ts +396 -0
  118. package/costcanary/lib/usage-limits.ts +164 -0
  119. package/costcanary/lib/utils.ts +6 -0
  120. package/costcanary/lib/websocket.ts +153 -0
  121. package/costcanary/lib/wrapped-keys.ts +531 -0
  122. package/costcanary/market-research.md +443 -0
  123. package/costcanary/middleware.ts +48 -0
  124. package/costcanary/next.config.js +43 -0
  125. package/costcanary/nia-sources.md +151 -0
  126. package/costcanary/package-lock.json +12162 -0
  127. package/costcanary/package.json +92 -0
  128. package/costcanary/package.json.backup +89 -0
  129. package/costcanary/postcss.config.js +6 -0
  130. package/costcanary/pricing-worker/.env.example +8 -0
  131. package/costcanary/pricing-worker/README.md +81 -0
  132. package/costcanary/pricing-worker/package-lock.json +1109 -0
  133. package/costcanary/pricing-worker/package.json +26 -0
  134. package/costcanary/pricing-worker/railway.json +13 -0
  135. package/costcanary/pricing-worker/schema.prisma +326 -0
  136. package/costcanary/pricing-worker/src/index.ts +115 -0
  137. package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
  138. package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
  139. package/costcanary/pricing-worker/test-tavily.ts +47 -0
  140. package/costcanary/pricing-worker/tsconfig.json +24 -0
  141. package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
  142. package/costcanary/prisma/schema.prisma +326 -0
  143. package/costcanary/prisma/seed-pricing.ts +133 -0
  144. package/costcanary/public/costhawk-logo.png +0 -0
  145. package/costcanary/railway.json +30 -0
  146. package/costcanary/railway.toml +16 -0
  147. package/costcanary/research-nia.md +298 -0
  148. package/costcanary/research.md +411 -0
  149. package/costcanary/scripts/build-production.js +65 -0
  150. package/costcanary/scripts/check-current-pricing.ts +51 -0
  151. package/costcanary/scripts/check-pricing-data.ts +174 -0
  152. package/costcanary/scripts/create-stripe-prices.js +49 -0
  153. package/costcanary/scripts/fix-pricing-data.ts +135 -0
  154. package/costcanary/scripts/fix-pricing-db.ts +148 -0
  155. package/costcanary/scripts/postinstall.js +58 -0
  156. package/costcanary/scripts/railway-deploy.sh +52 -0
  157. package/costcanary/scripts/run-migration.js +61 -0
  158. package/costcanary/scripts/start-production.js +175 -0
  159. package/costcanary/scripts/test-wrapped-key.ts +85 -0
  160. package/costcanary/scripts/validate-deployment.js +176 -0
  161. package/costcanary/scripts/validate-production.js +119 -0
  162. package/costcanary/server.js.backup +38 -0
  163. package/costcanary/tailwind.config.ts +216 -0
  164. package/costcanary/test-pricing-status.sh +27 -0
  165. package/costcanary/tsconfig.json +42 -0
  166. package/docs/sessions/session-2025-12-01.md +570 -0
  167. package/executive-summary.md +302 -0
  168. package/index.js +1 -0
  169. package/nia-sources.md +163 -0
  170. package/package.json +16 -0
  171. package/research.md +750 -0
@@ -0,0 +1,354 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { motion } from "framer-motion"
5
+ import { Download, TrendingUp, DollarSign, Activity, BarChart3 } from "lucide-react"
6
+ import {
7
+ BarChart, Bar, PieChart, Pie, Cell, Legend,
8
+ XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer
9
+ } from "recharts"
10
+ import { chartColors, providerColors, chartConfig } from "@/lib/chart-colors"
11
+
12
+ interface UsageData {
13
+ period: string
14
+ provider: string | null
15
+ usage: {
16
+ totalCost: number
17
+ totalRequests: number
18
+ avgLatency: number
19
+ byProvider: Array<{
20
+ provider: string
21
+ cost: number
22
+ requests: number
23
+ }>
24
+ }
25
+ budgets: Array<{
26
+ name: string
27
+ spent: number
28
+ limit: number
29
+ percentage: number
30
+ remaining: number
31
+ status: string
32
+ }>
33
+ }
34
+
35
+ // Use consistent provider colors from chart-colors.ts
36
+
37
+ export default function UsagePage() {
38
+ const [usageData, setUsageData] = useState<UsageData | null>(null)
39
+ const [loading, setLoading] = useState(true)
40
+ const [period, setPeriod] = useState<string>("DAILY")
41
+ const [selectedProvider, setSelectedProvider] = useState<string | null>(null)
42
+
43
+ useEffect(() => {
44
+ fetchUsageData()
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, [period, selectedProvider])
47
+
48
+ const fetchUsageData = async () => {
49
+ setLoading(true)
50
+ try {
51
+ const params = new URLSearchParams({
52
+ period,
53
+ ...(selectedProvider && { provider: selectedProvider }),
54
+ })
55
+ const response = await fetch(`/api/usage/current?${params}`)
56
+ const data = await response.json()
57
+ setUsageData(data)
58
+ } catch (error) {
59
+ console.error("Failed to fetch usage data:", error)
60
+ } finally {
61
+ setLoading(false)
62
+ }
63
+ }
64
+
65
+ const handleExportCSV = async () => {
66
+ // TODO: Implement CSV export
67
+ alert("CSV export coming soon!")
68
+ }
69
+
70
+ const formatCurrency = (value: number) => {
71
+ return new Intl.NumberFormat("en-US", {
72
+ style: "currency",
73
+ currency: "USD",
74
+ minimumFractionDigits: 2,
75
+ maximumFractionDigits: 4,
76
+ }).format(value)
77
+ }
78
+
79
+ // Prepare chart data - ensure values are numbers (Prisma returns Decimals)
80
+ const pieData = usageData?.usage?.byProvider?.map((p: any) => ({
81
+ name: p.provider,
82
+ value: Number(p.cost) || 0,
83
+ })) || []
84
+
85
+ const barData = usageData?.usage?.byProvider?.map((p: any) => ({
86
+ provider: p.provider,
87
+ requests: Number(p.requests) || 0,
88
+ cost: Number(p.cost) || 0,
89
+ })) || []
90
+
91
+ return (
92
+ <div className="space-y-6">
93
+ {/* Header */}
94
+ <div className="flex items-center justify-between">
95
+ <div>
96
+ <h1 className="text-3xl font-bold text-white">Usage Analytics</h1>
97
+ <p className="mt-2 text-charcoal-400">
98
+ Track your API usage and costs across all providers
99
+ </p>
100
+ </div>
101
+ <motion.button
102
+ whileHover={{ scale: 1.05 }}
103
+ whileTap={{ scale: 0.95 }}
104
+ onClick={handleExportCSV}
105
+ className="flex items-center space-x-2 rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white transition-colors hover:bg-charcoal-700"
106
+ >
107
+ <Download className="h-5 w-5" />
108
+ <span>Export CSV</span>
109
+ </motion.button>
110
+ </div>
111
+
112
+ {/* Filters */}
113
+ <div className="flex space-x-4">
114
+ <select
115
+ value={period}
116
+ onChange={(e) => setPeriod(e.target.value)}
117
+ className="rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white focus:border-coral-500 focus:outline-none"
118
+ >
119
+ <option value="DAILY">Today</option>
120
+ <option value="WEEKLY">This Week</option>
121
+ <option value="MONTHLY">This Month</option>
122
+ <option value="QUARTERLY">This Quarter</option>
123
+ <option value="YEARLY">This Year</option>
124
+ </select>
125
+
126
+ <select
127
+ value={selectedProvider || ""}
128
+ onChange={(e) => setSelectedProvider(e.target.value || null)}
129
+ className="rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white focus:border-coral-500 focus:outline-none"
130
+ >
131
+ <option value="">All Providers</option>
132
+ <option value="OPENAI">OpenAI</option>
133
+ <option value="ANTHROPIC">Anthropic</option>
134
+ <option value="GOOGLE">Google</option>
135
+ <option value="GROK">xAI Grok</option>
136
+ </select>
137
+ </div>
138
+
139
+ {loading ? (
140
+ <div className="space-y-4">
141
+ {[1, 2, 3].map((i) => (
142
+ <div key={i} className="h-32 animate-pulse rounded-lg bg-charcoal-900" />
143
+ ))}
144
+ </div>
145
+ ) : (
146
+ <>
147
+ {/* Stats Cards */}
148
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
149
+ <motion.div
150
+ initial={{ opacity: 0, y: 20 }}
151
+ animate={{ opacity: 1, y: 0 }}
152
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
153
+ >
154
+ <div className="flex items-center justify-between">
155
+ <div>
156
+ <p className="text-sm text-charcoal-400">Total Cost</p>
157
+ <p className="mt-2 text-2xl font-bold text-white">
158
+ {formatCurrency(usageData?.usage.totalCost || 0)}
159
+ </p>
160
+ </div>
161
+ <DollarSign className="h-8 w-8 text-coral-500" />
162
+ </div>
163
+ </motion.div>
164
+
165
+ <motion.div
166
+ initial={{ opacity: 0, y: 20 }}
167
+ animate={{ opacity: 1, y: 0 }}
168
+ transition={{ delay: 0.1 }}
169
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
170
+ >
171
+ <div className="flex items-center justify-between">
172
+ <div>
173
+ <p className="text-sm text-charcoal-400">Total Requests</p>
174
+ <p className="mt-2 text-2xl font-bold text-white">
175
+ {usageData?.usage.totalRequests.toLocaleString() || 0}
176
+ </p>
177
+ </div>
178
+ <BarChart3 className="h-8 w-8 text-coral-500" />
179
+ </div>
180
+ </motion.div>
181
+
182
+ <motion.div
183
+ initial={{ opacity: 0, y: 20 }}
184
+ animate={{ opacity: 1, y: 0 }}
185
+ transition={{ delay: 0.2 }}
186
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
187
+ >
188
+ <div className="flex items-center justify-between">
189
+ <div>
190
+ <p className="text-sm text-charcoal-400">Avg Latency</p>
191
+ <p className="mt-2 text-2xl font-bold text-white">
192
+ {usageData?.usage.avgLatency || 0}ms
193
+ </p>
194
+ </div>
195
+ <Activity className="h-8 w-8 text-coral-500" />
196
+ </div>
197
+ </motion.div>
198
+
199
+ <motion.div
200
+ initial={{ opacity: 0, y: 20 }}
201
+ animate={{ opacity: 1, y: 0 }}
202
+ transition={{ delay: 0.3 }}
203
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
204
+ >
205
+ <div className="flex items-center justify-between">
206
+ <div>
207
+ <p className="text-sm text-charcoal-400">Active Providers</p>
208
+ <p className="mt-2 text-2xl font-bold text-white">
209
+ {usageData?.usage.byProvider.length || 0}
210
+ </p>
211
+ </div>
212
+ <TrendingUp className="h-8 w-8 text-coral-500" />
213
+ </div>
214
+ </motion.div>
215
+ </div>
216
+
217
+ {/* Charts */}
218
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
219
+ {/* Cost by Provider Pie Chart */}
220
+ <motion.div
221
+ initial={{ opacity: 0, scale: 0.95 }}
222
+ animate={{ opacity: 1, scale: 1 }}
223
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
224
+ >
225
+ <h3 className="mb-4 text-lg font-medium text-white">Cost by Provider</h3>
226
+ {pieData.length > 0 ? (
227
+ <ResponsiveContainer width="100%" height={300}>
228
+ <PieChart>
229
+ <Pie
230
+ data={pieData}
231
+ cx="50%"
232
+ cy="45%"
233
+ innerRadius={50}
234
+ outerRadius={90}
235
+ dataKey="value"
236
+ nameKey="name"
237
+ fill={chartColors.coral}
238
+ >
239
+ {pieData.map((entry, index) => (
240
+ <Cell
241
+ key={`cell-${index}`}
242
+ fill={providerColors[entry.name] || chartColors.coral}
243
+ />
244
+ ))}
245
+ </Pie>
246
+ <Tooltip
247
+ content={({ active, payload }) => {
248
+ if (active && payload && payload.length) {
249
+ const data = payload[0].payload
250
+ return (
251
+ <div style={{
252
+ backgroundColor: '#2D2F36',
253
+ border: '1px solid #3D3F47',
254
+ borderRadius: '8px',
255
+ padding: '12px',
256
+ }}>
257
+ <p style={{ color: '#F8FAFC', fontWeight: 600, marginBottom: '4px' }}>
258
+ {data.name}
259
+ </p>
260
+ <p style={{ color: '#CBD5E1' }}>
261
+ Cost: {formatCurrency(data.value)}
262
+ </p>
263
+ </div>
264
+ )
265
+ }
266
+ return null
267
+ }}
268
+ />
269
+ <Legend
270
+ verticalAlign="bottom"
271
+ height={36}
272
+ formatter={(value) => <span style={{ color: '#F8FAFC' }}>{value}</span>}
273
+ />
274
+ </PieChart>
275
+ </ResponsiveContainer>
276
+ ) : (
277
+ <div className="flex h-[300px] items-center justify-center text-charcoal-500">
278
+ No data available
279
+ </div>
280
+ )}
281
+ </motion.div>
282
+
283
+ {/* Requests by Provider Bar Chart */}
284
+ <motion.div
285
+ initial={{ opacity: 0, scale: 0.95 }}
286
+ animate={{ opacity: 1, scale: 1 }}
287
+ transition={{ delay: 0.1 }}
288
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
289
+ >
290
+ <h3 className="mb-4 text-lg font-medium text-white">Requests by Provider</h3>
291
+ {barData.length > 0 ? (
292
+ <ResponsiveContainer width="100%" height={300}>
293
+ <BarChart data={barData}>
294
+ <CartesianGrid {...chartConfig.grid} />
295
+ <XAxis dataKey="provider" {...chartConfig.xAxis} />
296
+ <YAxis {...chartConfig.yAxis} />
297
+ <Tooltip
298
+ contentStyle={chartConfig.tooltip.contentStyle}
299
+ labelStyle={chartConfig.tooltip.labelStyle}
300
+ />
301
+ <Bar dataKey="requests" fill={chartColors.coral} radius={[6, 6, 0, 0]} />
302
+ </BarChart>
303
+ </ResponsiveContainer>
304
+ ) : (
305
+ <div className="flex h-[300px] items-center justify-center text-charcoal-500">
306
+ No data available
307
+ </div>
308
+ )}
309
+ </motion.div>
310
+ </div>
311
+
312
+ {/* Budget Status */}
313
+ {usageData?.budgets && usageData.budgets.length > 0 && (
314
+ <motion.div
315
+ initial={{ opacity: 0, y: 20 }}
316
+ animate={{ opacity: 1, y: 0 }}
317
+ className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
318
+ >
319
+ <h3 className="mb-4 text-lg font-medium text-white">Budget Status</h3>
320
+ <div className="space-y-4">
321
+ {usageData.budgets.map((budget, index) => (
322
+ <div key={index}>
323
+ <div className="flex items-center justify-between text-sm">
324
+ <span className="text-charcoal-400">{budget.name}</span>
325
+ <span className={`font-medium ${
326
+ budget.status === "exceeded" ? "text-red-500" :
327
+ budget.status === "warning" ? "text-yellow-500" :
328
+ "text-green-500"
329
+ }`}>
330
+ {formatCurrency(budget.spent)} / {formatCurrency(budget.limit)}
331
+ </span>
332
+ </div>
333
+ <div className="mt-2 h-2 overflow-hidden rounded-full bg-charcoal-800">
334
+ <motion.div
335
+ initial={{ width: 0 }}
336
+ animate={{ width: `${Math.min(budget.percentage, 100)}%` }}
337
+ transition={{ duration: 0.5, ease: "easeOut" }}
338
+ className={`h-full ${
339
+ budget.status === "exceeded" ? "bg-red-500" :
340
+ budget.status === "warning" ? "bg-yellow-500" :
341
+ "bg-green-500"
342
+ }`}
343
+ />
344
+ </div>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ </motion.div>
349
+ )}
350
+ </>
351
+ )}
352
+ </div>
353
+ )
354
+ }