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,349 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, Suspense } from 'react';
4
+ import { useAuth } from '@clerk/nextjs';
5
+ // Card components removed - using motion.div for animations
6
+ import { Button } from '@/components/ui/button';
7
+ import { Badge } from '@/components/ui/badge';
8
+ import { Progress } from '@/components/ui/progress';
9
+ import { Alert, AlertDescription } from '@/components/ui/alert';
10
+ import {
11
+ CreditCard,
12
+ TrendingUp,
13
+ AlertCircle,
14
+ CheckCircle,
15
+ Zap,
16
+ Calendar,
17
+ BarChart3
18
+ } from 'lucide-react';
19
+ import { PRICING_TIERS } from '@/lib/stripe-client';
20
+ import { useRouter, useSearchParams } from 'next/navigation';
21
+ import { motion } from 'framer-motion';
22
+
23
+ interface SubscriptionStatus {
24
+ tier: string;
25
+ hasSubscription: boolean;
26
+ subscriptionStatus?: string;
27
+ subscriptionEndDate?: string;
28
+ }
29
+
30
+ interface UsageData {
31
+ current: number;
32
+ limit: number | null;
33
+ percentage: number;
34
+ }
35
+
36
+ function BillingPageContent() {
37
+ const { userId } = useAuth();
38
+ const router = useRouter();
39
+ const searchParams = useSearchParams();
40
+ const [loading, setLoading] = useState(true);
41
+ const [subscription, setSubscription] = useState<SubscriptionStatus | null>(null);
42
+ const [usage, setUsage] = useState<UsageData | null>(null);
43
+ const [processingPortal, setProcessingPortal] = useState(false);
44
+
45
+ const success = searchParams.get('success') === 'true';
46
+
47
+ useEffect(() => {
48
+ if (userId) {
49
+ fetchSubscriptionStatus();
50
+ fetchUsageData();
51
+ }
52
+ }, [userId]);
53
+
54
+ const fetchSubscriptionStatus = async () => {
55
+ try {
56
+ const response = await fetch('/api/stripe/checkout');
57
+ const data = await response.json();
58
+ setSubscription(data);
59
+ } catch (error) {
60
+ console.error('Failed to fetch subscription status:', error);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ };
65
+
66
+ const fetchUsageData = async () => {
67
+ try {
68
+ const response = await fetch('/api/usage/summary');
69
+ if (response.ok) {
70
+ const data = await response.json();
71
+ setUsage(data);
72
+ }
73
+ } catch (error) {
74
+ console.error('Failed to fetch usage data:', error);
75
+ }
76
+ };
77
+
78
+ const handleManageSubscription = async () => {
79
+ setProcessingPortal(true);
80
+ try {
81
+ const response = await fetch('/api/stripe/checkout', {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ },
86
+ body: JSON.stringify({
87
+ tierId: 'portal', // Special flag for portal
88
+ returnUrl: window.location.href,
89
+ }),
90
+ });
91
+
92
+ const data = await response.json();
93
+
94
+ if (data.url) {
95
+ window.location.href = data.url;
96
+ }
97
+ } catch (error) {
98
+ console.error('Failed to open billing portal:', error);
99
+ } finally {
100
+ setProcessingPortal(false);
101
+ }
102
+ };
103
+
104
+ const handleUpgrade = () => {
105
+ router.push('/pricing');
106
+ };
107
+
108
+ if (loading) {
109
+ return (
110
+ <div className="flex items-center justify-center min-h-[400px]">
111
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500" />
112
+ </div>
113
+ );
114
+ }
115
+
116
+ const currentTier = PRICING_TIERS.find(t => t.id === subscription?.tier) || PRICING_TIERS[0];
117
+ const isFreeTier = currentTier.id === 'free';
118
+
119
+ return (
120
+ <div className="space-y-6">
121
+ {/* Header */}
122
+ <div>
123
+ <h1 className="text-3xl font-bold text-white">Billing & Subscription</h1>
124
+ <p className="mt-2 text-slate-400">
125
+ Manage your subscription, billing, and usage
126
+ </p>
127
+ </div>
128
+
129
+ {success && (
130
+ <Alert className="border-green-500 bg-green-500/10">
131
+ <CheckCircle className="h-4 w-4 text-green-500" />
132
+ <AlertDescription className="text-green-400">
133
+ Payment successful! Your subscription is now active.
134
+ </AlertDescription>
135
+ </Alert>
136
+ )}
137
+
138
+ <div className="grid gap-6 md:grid-cols-2">
139
+ {/* Current Plan Card */}
140
+ <motion.div
141
+ initial={{ opacity: 0, y: 20 }}
142
+ animate={{ opacity: 1, y: 0 }}
143
+ className="rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm"
144
+ >
145
+ <div className="mb-4 flex items-center justify-between">
146
+ <h2 className="text-xl font-bold text-white">Current Plan</h2>
147
+ <Badge
148
+ variant={isFreeTier ? 'secondary' : 'default'}
149
+ className={isFreeTier ? 'bg-slate-700' : 'bg-purple-500'}
150
+ >
151
+ {currentTier.name}
152
+ </Badge>
153
+ </div>
154
+
155
+ <div className="space-y-4">
156
+ <div className="flex items-center justify-between">
157
+ <span className="text-2xl font-bold text-white">
158
+ ${currentTier.price}
159
+ {currentTier.price > 0 && <span className="text-sm font-normal text-slate-400">/month</span>}
160
+ </span>
161
+ {subscription?.subscriptionStatus === 'trialing' && (
162
+ <Badge variant="outline" className="border-blue-500 text-blue-400">
163
+ <Zap className="w-3 h-3 mr-1" />
164
+ Trial Active
165
+ </Badge>
166
+ )}
167
+ </div>
168
+
169
+ {subscription?.subscriptionEndDate && (
170
+ <div className="flex items-center gap-2 text-sm text-slate-400">
171
+ <Calendar className="w-4 h-4" />
172
+ <span>
173
+ {subscription.subscriptionStatus === 'active'
174
+ ? 'Renews'
175
+ : subscription.subscriptionStatus === 'trialing'
176
+ ? 'Trial ends'
177
+ : 'Expires'} on {new Date(subscription.subscriptionEndDate).toLocaleDateString()}
178
+ </span>
179
+ </div>
180
+ )}
181
+
182
+ <div className="space-y-2 border-t border-slate-800 pt-4">
183
+ {currentTier.features.slice(0, 3).map((feature, i) => (
184
+ <div key={i} className="flex items-center gap-2 text-sm text-slate-300">
185
+ <CheckCircle className="w-4 h-4 text-green-500" />
186
+ <span>{feature}</span>
187
+ </div>
188
+ ))}
189
+ </div>
190
+
191
+ <div className="flex gap-3 pt-4">
192
+ {subscription?.hasSubscription ? (
193
+ <Button
194
+ onClick={handleManageSubscription}
195
+ disabled={processingPortal}
196
+ variant="outline"
197
+ className="flex-1 border-slate-700 hover:bg-slate-800"
198
+ >
199
+ <CreditCard className="w-4 h-4 mr-2" />
200
+ {processingPortal ? 'Loading...' : 'Manage Billing'}
201
+ </Button>
202
+ ) : (
203
+ <Button
204
+ onClick={handleUpgrade}
205
+ className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
206
+ >
207
+ <TrendingUp className="w-4 h-4 mr-2" />
208
+ Upgrade Plan
209
+ </Button>
210
+ )}
211
+ </div>
212
+ </div>
213
+ </motion.div>
214
+
215
+ {/* Usage Card */}
216
+ <motion.div
217
+ initial={{ opacity: 0, y: 20 }}
218
+ animate={{ opacity: 1, y: 0 }}
219
+ transition={{ delay: 0.1 }}
220
+ className="rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm"
221
+ >
222
+ <div className="mb-4 flex items-center justify-between">
223
+ <h2 className="text-xl font-bold text-white">Monthly Usage</h2>
224
+ <BarChart3 className="w-5 h-5 text-slate-400" />
225
+ </div>
226
+
227
+ {usage ? (
228
+ <div className="space-y-4">
229
+ <div className="space-y-2">
230
+ <div className="flex justify-between text-sm">
231
+ <span className="text-slate-400">API Calls</span>
232
+ <span className="font-semibold text-white">
233
+ {usage.current.toLocaleString()}
234
+ {usage.limit && ` / ${usage.limit.toLocaleString()}`}
235
+ {!usage.limit && ' (Unlimited)'}
236
+ </span>
237
+ </div>
238
+
239
+ {usage.limit && (
240
+ <>
241
+ <Progress value={usage.percentage} className="h-2 bg-slate-800" />
242
+
243
+ {usage.percentage >= 90 && (
244
+ <Alert className="mt-3 border-orange-500 bg-orange-500/10">
245
+ <AlertCircle className="h-4 w-4 text-orange-500" />
246
+ <AlertDescription className="text-orange-400">
247
+ You&apos;ve used {usage.percentage}% of your monthly limit.
248
+ Consider upgrading for unlimited tracking.
249
+ </AlertDescription>
250
+ </Alert>
251
+ )}
252
+ </>
253
+ )}
254
+ </div>
255
+
256
+ <div className="border-t border-slate-800 pt-4">
257
+ <div className="grid grid-cols-2 gap-4 text-sm">
258
+ <div>
259
+ <p className="text-slate-500 mb-1">Resets in</p>
260
+ <p className="font-semibold text-white">
261
+ {(() => {
262
+ const now = new Date();
263
+ const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
264
+ const days = Math.ceil((nextMonth.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
265
+ return `${days} days`;
266
+ })()}
267
+ </p>
268
+ </div>
269
+ <div>
270
+ <p className="text-slate-500 mb-1">Daily average</p>
271
+ <p className="font-semibold text-white">
272
+ {Math.round((usage?.current || 0) / new Date().getDate()).toLocaleString()}
273
+ </p>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ ) : (
279
+ <div className="text-center py-8 text-slate-500">
280
+ <BarChart3 className="w-12 h-12 mx-auto mb-3 opacity-50" />
281
+ <p>No usage data available yet</p>
282
+ </div>
283
+ )}
284
+ </motion.div>
285
+
286
+ {/* Upgrade Card for Free Users */}
287
+ {isFreeTier && (
288
+ <motion.div
289
+ initial={{ opacity: 0, y: 20 }}
290
+ animate={{ opacity: 1, y: 0 }}
291
+ transition={{ delay: 0.2 }}
292
+ className="md:col-span-2 rounded-lg border border-purple-500/30 bg-gradient-to-br from-purple-900/20 to-pink-900/20 p-6"
293
+ >
294
+ <div className="flex items-start justify-between">
295
+ <div className="flex-1">
296
+ <h3 className="text-xl font-bold text-white mb-2 flex items-center gap-2">
297
+ <Zap className="w-5 h-5 text-purple-400" />
298
+ Unlock Pro Features
299
+ </h3>
300
+ <p className="text-slate-400 mb-4">
301
+ Get unlimited tracking and advanced features
302
+ </p>
303
+ <ul className="space-y-2 mb-6">
304
+ <li className="flex items-center gap-2 text-sm text-slate-300">
305
+ <CheckCircle className="w-4 h-4 text-green-500" />
306
+ Unlimited API calls
307
+ </li>
308
+ <li className="flex items-center gap-2 text-sm text-slate-300">
309
+ <CheckCircle className="w-4 h-4 text-green-500" />
310
+ 90-day history
311
+ </li>
312
+ <li className="flex items-center gap-2 text-sm text-slate-300">
313
+ <CheckCircle className="w-4 h-4 text-green-500" />
314
+ Advanced analytics & insights
315
+ </li>
316
+ </ul>
317
+ </div>
318
+ <div className="text-right">
319
+ <div className="text-3xl font-bold text-white mb-1">$9.99</div>
320
+ <div className="text-sm text-slate-400 mb-4">/month</div>
321
+ <Button
322
+ onClick={handleUpgrade}
323
+ className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
324
+ >
325
+ Upgrade Now
326
+ </Button>
327
+ <p className="text-xs text-slate-500 mt-2">
328
+ 14-day free trial
329
+ </p>
330
+ </div>
331
+ </div>
332
+ </motion.div>
333
+ )}
334
+ </div>
335
+ </div>
336
+ );
337
+ }
338
+
339
+ export default function BillingPage() {
340
+ return (
341
+ <Suspense fallback={
342
+ <div className="flex items-center justify-center min-h-[400px]">
343
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500" />
344
+ </div>
345
+ }>
346
+ <BillingPageContent />
347
+ </Suspense>
348
+ );
349
+ }
@@ -0,0 +1,188 @@
1
+ "use client"
2
+
3
+ import { UserButton, SignOutButton } from "@clerk/nextjs"
4
+ import { BarChart3, Home, Settings, CreditCard, Bell, Shield, Beaker, LogOut, Key, ShieldAlert, Activity, Database, Wrench } from "lucide-react"
5
+ import Link from "next/link"
6
+ import { usePathname } from "next/navigation"
7
+ import { Button } from "@/components/ui/button"
8
+ import { useState, useEffect } from "react"
9
+ import { Separator } from "@/components/ui/separator"
10
+
11
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
12
+ const pathname = usePathname()
13
+ const [isAdmin, setIsAdmin] = useState(false)
14
+ const [loading, setLoading] = useState(true)
15
+
16
+ // Check admin status on mount
17
+ useEffect(() => {
18
+ const checkAdminStatus = async () => {
19
+ try {
20
+ const response = await fetch('/api/admin/whoami')
21
+ if (response.ok) {
22
+ const data = await response.json()
23
+ setIsAdmin(data.isAdmin === true)
24
+ }
25
+ } catch (error) {
26
+ console.error('Failed to check admin status:', error)
27
+ } finally {
28
+ setLoading(false)
29
+ }
30
+ }
31
+ checkAdminStatus()
32
+ }, [])
33
+
34
+ return (
35
+ <div className="flex h-screen bg-slate-950">
36
+ <aside className="w-64 border-r border-slate-800 bg-slate-900 relative">
37
+ <div className="flex h-16 items-center border-b border-slate-800 px-6">
38
+ <Link href="/dashboard" className="flex items-center space-x-2">
39
+ <img src="/costhawk-logo.png" alt="CostHawk" className="h-10 w-auto" />
40
+ <span className="text-xl font-bold text-white">CostHawk</span>
41
+ </Link>
42
+ </div>
43
+
44
+ <nav className="space-y-1 p-4 pb-20">
45
+ <NavItem href="/dashboard" icon={<Home />} label="Dashboard" active={pathname === '/dashboard'} />
46
+ <NavItem href="/dashboard/wrapped-keys" icon={<Key />} label="Wrapped Keys" active={pathname === '/dashboard/wrapped-keys'} highlight />
47
+ <NavItem href="/dashboard/playground" icon={<Beaker />} label="Playground" active={pathname === '/dashboard/playground'} />
48
+ <NavItem href="/dashboard/usage" icon={<BarChart3 />} label="Usage" active={pathname === '/dashboard/usage'} />
49
+ <NavItem href="/dashboard/api-keys" icon={<Shield />} label="API Keys" active={pathname === '/dashboard/api-keys'} />
50
+ <NavItem href="/dashboard/billing" icon={<CreditCard />} label="Billing" active={pathname === '/dashboard/billing'} />
51
+ <NavItem href="/dashboard/alerts" icon={<Bell />} label="Alerts" active={pathname === '/dashboard/alerts'} />
52
+ <NavItem href="/dashboard/settings" icon={<Settings />} label="Settings" active={pathname === '/dashboard/settings'} />
53
+
54
+ {/* Admin Panel Section - Only visible to admins */}
55
+ {!loading && isAdmin && (
56
+ <>
57
+ <div className="pt-4 pb-2">
58
+ <Separator className="bg-slate-700" />
59
+ </div>
60
+ <div className="px-3 py-2">
61
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Admin Panel</h3>
62
+ </div>
63
+ <NavItem
64
+ href="/dashboard/admin"
65
+ icon={<ShieldAlert />}
66
+ label="Admin Overview"
67
+ active={pathname === '/dashboard/admin'}
68
+ />
69
+ <NavItem
70
+ href="/admin/pricing-status"
71
+ icon={<Activity />}
72
+ label="Pricing Status"
73
+ active={pathname === '/admin/pricing-status'}
74
+ />
75
+ <NavItem
76
+ href="/api/admin/trigger-pricing"
77
+ icon={<Database />}
78
+ label="Trigger Discovery"
79
+ active={pathname.includes('/trigger-pricing')}
80
+ external
81
+ />
82
+ <NavItem
83
+ href="/api/admin/fix-pricing"
84
+ icon={<Wrench />}
85
+ label="Fix Pricing Data"
86
+ active={pathname.includes('/fix-pricing')}
87
+ external
88
+ />
89
+ </>
90
+ )}
91
+ </nav>
92
+
93
+ {/* Sign Out Button at Bottom */}
94
+ <div className="absolute bottom-4 left-4 right-4">
95
+ <SignOutButton redirectUrl="/">
96
+ <Button
97
+ variant="ghost"
98
+ className="w-full justify-start text-slate-400 hover:bg-red-900/20 hover:text-red-400 transition-colors"
99
+ >
100
+ <LogOut className="h-5 w-5 mr-3" />
101
+ Sign Out
102
+ </Button>
103
+ </SignOutButton>
104
+ </div>
105
+ </aside>
106
+
107
+ <div className="flex flex-1 flex-col">
108
+ <header className="flex h-16 items-center justify-between border-b border-slate-800 bg-slate-900 px-6">
109
+ <h1 className="text-xl font-semibold text-white">Dashboard</h1>
110
+ <div className="flex items-center space-x-4">
111
+ <span className="text-sm text-slate-400 hidden sm:block">Click avatar to sign out</span>
112
+ <UserButton
113
+ afterSignOutUrl="/"
114
+ appearance={{
115
+ elements: {
116
+ avatarBox: "w-10 h-10 ring-2 ring-primary-500/50 hover:ring-primary-400 transition-all",
117
+ userButtonPopoverCard: "bg-slate-900 border-slate-700",
118
+ userButtonPopoverActionButton: "text-slate-300 hover:text-white hover:bg-slate-800",
119
+ userButtonPopoverActionButtonText: "text-slate-300",
120
+ userButtonPopoverFooter: "hidden"
121
+ }
122
+ }}
123
+ />
124
+ </div>
125
+ </header>
126
+
127
+ <main className="flex-1 overflow-auto bg-slate-950 p-6">{children}</main>
128
+ </div>
129
+ </div>
130
+ )
131
+ }
132
+
133
+ function NavItem({
134
+ href,
135
+ icon,
136
+ label,
137
+ active = false,
138
+ highlight = false,
139
+ external = false,
140
+ }: {
141
+ href: string
142
+ icon: React.ReactNode
143
+ label: string
144
+ active?: boolean
145
+ highlight?: boolean
146
+ external?: boolean
147
+ }) {
148
+ const className = `flex items-center space-x-3 rounded-lg px-3 py-2 transition-colors ${
149
+ active
150
+ ? "bg-primary-600/20 text-primary-500"
151
+ : highlight
152
+ ? "text-white bg-gradient-to-r from-primary-600/20 to-primary-500/20 border border-primary-500/30 hover:from-primary-600/30 hover:to-primary-500/30"
153
+ : "text-slate-400 hover:bg-slate-800 hover:text-white"
154
+ }`
155
+
156
+ // For external API links, use regular anchor tag
157
+ if (external) {
158
+ return (
159
+ <a
160
+ href={href}
161
+ target="_blank"
162
+ rel="noopener noreferrer"
163
+ className={className}
164
+ >
165
+ <span className="h-5 w-5">{icon}</span>
166
+ <span>{label}</span>
167
+ <svg className="ml-auto h-3 w-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
168
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
169
+ </svg>
170
+ </a>
171
+ )
172
+ }
173
+
174
+ return (
175
+ <Link
176
+ href={href}
177
+ className={className}
178
+ >
179
+ <span className="h-5 w-5">{icon}</span>
180
+ <span>{label}</span>
181
+ {highlight && (
182
+ <span className="ml-auto rounded-full bg-primary-500 px-2 py-0.5 text-xs font-medium text-white">
183
+ NEW
184
+ </span>
185
+ )}
186
+ </Link>
187
+ )
188
+ }
@@ -0,0 +1,13 @@
1
+ import { auth } from "@clerk/nextjs/server"
2
+ import { redirect } from "next/navigation"
3
+ import { DashboardContent } from "@/components/dashboard/dashboard-content"
4
+
5
+ export default async function DashboardPage() {
6
+ const { userId } = await auth()
7
+
8
+ if (!userId) {
9
+ redirect("/sign-in")
10
+ }
11
+
12
+ return <DashboardContent />
13
+ }