shipd 0.1.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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1366 -0
  5. package/docs-template/README.md +255 -0
  6. package/docs-template/[slug]/[subslug]/page.tsx +1242 -0
  7. package/docs-template/[slug]/page.tsx +422 -0
  8. package/docs-template/api/page.tsx +47 -0
  9. package/docs-template/components/docs/docs-category-page.tsx +162 -0
  10. package/docs-template/components/docs/docs-code-card.tsx +135 -0
  11. package/docs-template/components/docs/docs-header.tsx +69 -0
  12. package/docs-template/components/docs/docs-nav.ts +95 -0
  13. package/docs-template/components/docs/docs-sidebar.tsx +112 -0
  14. package/docs-template/components/docs/docs-toc.tsx +38 -0
  15. package/docs-template/components/ui/badge.tsx +47 -0
  16. package/docs-template/components/ui/button.tsx +60 -0
  17. package/docs-template/components/ui/card.tsx +93 -0
  18. package/docs-template/components/ui/sheet.tsx +140 -0
  19. package/docs-template/documentation/page.tsx +80 -0
  20. package/docs-template/layout.tsx +27 -0
  21. package/docs-template/lib/utils.ts +7 -0
  22. package/docs-template/page.tsx +360 -0
  23. package/package.json +66 -0
  24. package/template/.env.example +45 -0
  25. package/template/README.md +239 -0
  26. package/template/app/api/auth/[...all]/route.ts +4 -0
  27. package/template/app/api/chat/route.ts +16 -0
  28. package/template/app/api/subscription/route.ts +25 -0
  29. package/template/app/api/upload-image/route.ts +64 -0
  30. package/template/app/blog/[slug]/page.tsx +314 -0
  31. package/template/app/blog/page.tsx +107 -0
  32. package/template/app/dashboard/_components/chart-interactive.tsx +289 -0
  33. package/template/app/dashboard/_components/chatbot.tsx +39 -0
  34. package/template/app/dashboard/_components/mode-toggle.tsx +46 -0
  35. package/template/app/dashboard/_components/navbar.tsx +84 -0
  36. package/template/app/dashboard/_components/section-cards.tsx +102 -0
  37. package/template/app/dashboard/_components/sidebar.tsx +90 -0
  38. package/template/app/dashboard/_components/subscribe-button.tsx +49 -0
  39. package/template/app/dashboard/billing/page.tsx +277 -0
  40. package/template/app/dashboard/chat/page.tsx +73 -0
  41. package/template/app/dashboard/cli/page.tsx +260 -0
  42. package/template/app/dashboard/layout.tsx +24 -0
  43. package/template/app/dashboard/page.tsx +216 -0
  44. package/template/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  45. package/template/app/dashboard/payment/page.tsx +126 -0
  46. package/template/app/dashboard/settings/page.tsx +613 -0
  47. package/template/app/dashboard/upload/page.tsx +324 -0
  48. package/template/app/error.tsx +78 -0
  49. package/template/app/favicon.ico +0 -0
  50. package/template/app/globals.css +126 -0
  51. package/template/app/layout.tsx +135 -0
  52. package/template/app/not-found.tsx +45 -0
  53. package/template/app/page.tsx +28 -0
  54. package/template/app/pricing/_component/pricing-table.tsx +276 -0
  55. package/template/app/pricing/page.tsx +23 -0
  56. package/template/app/privacy-policy/page.tsx +280 -0
  57. package/template/app/robots.txt +12 -0
  58. package/template/app/sign-in/page.tsx +228 -0
  59. package/template/app/sign-up/page.tsx +243 -0
  60. package/template/app/sitemap.ts +62 -0
  61. package/template/app/success/page.tsx +123 -0
  62. package/template/app/terms-of-service/page.tsx +212 -0
  63. package/template/auth-schema.ts +47 -0
  64. package/template/components/homepage/cli-workflow-section.tsx +138 -0
  65. package/template/components/homepage/features-section.tsx +150 -0
  66. package/template/components/homepage/footer.tsx +53 -0
  67. package/template/components/homepage/hero-section.tsx +112 -0
  68. package/template/components/homepage/integrations.tsx +124 -0
  69. package/template/components/homepage/navigation.tsx +116 -0
  70. package/template/components/homepage/news-section.tsx +82 -0
  71. package/template/components/homepage/testimonials-section.tsx +34 -0
  72. package/template/components/logos/BetterAuth.tsx +21 -0
  73. package/template/components/logos/NeonPostgres.tsx +41 -0
  74. package/template/components/logos/Nextjs.tsx +72 -0
  75. package/template/components/logos/Polar.tsx +7 -0
  76. package/template/components/logos/TailwindCSS.tsx +27 -0
  77. package/template/components/logos/index.ts +6 -0
  78. package/template/components/logos/shadcnui.tsx +8 -0
  79. package/template/components/provider.tsx +8 -0
  80. package/template/components/ui/avatar.tsx +53 -0
  81. package/template/components/ui/badge.tsx +46 -0
  82. package/template/components/ui/button.tsx +59 -0
  83. package/template/components/ui/card.tsx +92 -0
  84. package/template/components/ui/chart.tsx +353 -0
  85. package/template/components/ui/checkbox.tsx +32 -0
  86. package/template/components/ui/dialog.tsx +135 -0
  87. package/template/components/ui/dropdown-menu.tsx +257 -0
  88. package/template/components/ui/form.tsx +167 -0
  89. package/template/components/ui/input.tsx +21 -0
  90. package/template/components/ui/label.tsx +24 -0
  91. package/template/components/ui/progress.tsx +31 -0
  92. package/template/components/ui/resizable.tsx +56 -0
  93. package/template/components/ui/select.tsx +185 -0
  94. package/template/components/ui/separator.tsx +28 -0
  95. package/template/components/ui/sheet.tsx +139 -0
  96. package/template/components/ui/skeleton.tsx +13 -0
  97. package/template/components/ui/sonner.tsx +25 -0
  98. package/template/components/ui/switch.tsx +31 -0
  99. package/template/components/ui/tabs.tsx +66 -0
  100. package/template/components/ui/textarea.tsx +18 -0
  101. package/template/components/ui/toggle-group.tsx +73 -0
  102. package/template/components/ui/toggle.tsx +47 -0
  103. package/template/components/ui/tooltip.tsx +61 -0
  104. package/template/components/user-profile.tsx +139 -0
  105. package/template/components.json +21 -0
  106. package/template/db/drizzle.ts +14 -0
  107. package/template/db/migrations/0000_worried_rawhide_kid.sql +77 -0
  108. package/template/db/migrations/meta/0000_snapshot.json +494 -0
  109. package/template/db/migrations/meta/_journal.json +13 -0
  110. package/template/db/schema.ts +85 -0
  111. package/template/drizzle.config.ts +13 -0
  112. package/template/emails/components/layout.tsx +181 -0
  113. package/template/emails/password-reset.tsx +67 -0
  114. package/template/emails/payment-failed.tsx +167 -0
  115. package/template/emails/subscription-confirmation.tsx +129 -0
  116. package/template/emails/welcome.tsx +100 -0
  117. package/template/eslint.config.mjs +16 -0
  118. package/template/hooks/use-mobile.ts +21 -0
  119. package/template/lib/auth-client.ts +8 -0
  120. package/template/lib/auth.ts +276 -0
  121. package/template/lib/email.ts +118 -0
  122. package/template/lib/polar-products.ts +49 -0
  123. package/template/lib/subscription.ts +148 -0
  124. package/template/lib/upload-image.ts +28 -0
  125. package/template/lib/utils.ts +6 -0
  126. package/template/middleware.ts +30 -0
  127. package/template/next-env.d.ts +5 -0
  128. package/template/next.config.ts +27 -0
  129. package/template/package.json +99 -0
  130. package/template/postcss.config.mjs +5 -0
  131. package/template/public/add.png +0 -0
  132. package/template/public/favicon.svg +4 -0
  133. package/template/public/file.svg +1 -0
  134. package/template/public/globe.svg +1 -0
  135. package/template/public/iphone.png +0 -0
  136. package/template/public/logo.png +0 -0
  137. package/template/public/next.svg +1 -0
  138. package/template/public/polar-sh.svg +1 -0
  139. package/template/public/shadcn-ui.svg +1 -0
  140. package/template/public/site.webmanifest +21 -0
  141. package/template/public/vercel.svg +1 -0
  142. package/template/public/window.svg +1 -0
  143. package/template/tailwind.config.ts +89 -0
  144. package/template/template.config.json +138 -0
  145. package/template/tsconfig.json +27 -0
@@ -0,0 +1,613 @@
1
+ "use client";
2
+
3
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Label } from "@/components/ui/label";
15
+ import { Skeleton } from "@/components/ui/skeleton";
16
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
17
+ import { authClient } from "@/lib/auth-client";
18
+ import { ExternalLink, Settings2 } from "lucide-react";
19
+ import { useRouter, useSearchParams } from "next/navigation";
20
+ import { Suspense, useEffect, useState } from "react";
21
+ import { toast } from "sonner";
22
+
23
+ interface User {
24
+ id: string;
25
+ name: string;
26
+ email: string;
27
+ image?: string | null;
28
+ }
29
+
30
+ interface OrderItem {
31
+ label: string;
32
+ amount: number;
33
+ }
34
+
35
+ interface Order {
36
+ id: string;
37
+ product?: {
38
+ name: string;
39
+ };
40
+ createdAt: string;
41
+ totalAmount: number;
42
+ currency: string;
43
+ status: string;
44
+ subscription?: {
45
+ status: string;
46
+ endedAt?: string;
47
+ };
48
+ items: OrderItem[];
49
+ }
50
+
51
+ interface OrdersResponse {
52
+ result: {
53
+ items: Order[];
54
+ };
55
+ }
56
+
57
+ function SettingsContent() {
58
+ const [user, setUser] = useState<User | null>(null);
59
+ const [orders, setOrders] = useState<OrdersResponse | null>(null);
60
+ const [loading, setLoading] = useState(true);
61
+ const [currentTab, setCurrentTab] = useState("profile");
62
+ const router = useRouter();
63
+ const searchParams = useSearchParams();
64
+
65
+ // Profile form states
66
+ const [name, setName] = useState("");
67
+ const [email, setEmail] = useState("");
68
+
69
+ // Profile picture upload states
70
+ const [profileImage, setProfileImage] = useState<File | null>(null);
71
+ const [imagePreview, setImagePreview] = useState<string | null>(null);
72
+ const [uploadingImage, setUploadingImage] = useState(false);
73
+
74
+ const { data: organizations } = authClient.useListOrganizations();
75
+
76
+ // Handle URL tab parameter
77
+ useEffect(() => {
78
+ const tab = searchParams.get("tab");
79
+ if (tab && ["profile", "organization", "billing"].includes(tab)) {
80
+ setCurrentTab(tab);
81
+ }
82
+ }, [searchParams]);
83
+
84
+ useEffect(() => {
85
+ const fetchData = async () => {
86
+ try {
87
+ // Get user session
88
+ const session = await authClient.getSession();
89
+ if (session.data?.user) {
90
+ setUser(session.data.user);
91
+ setName(session.data.user.name || "");
92
+ setEmail(session.data.user.email || "");
93
+ }
94
+
95
+ // Try to fetch orders and customer state with better error handling
96
+ try {
97
+ const ordersResponse = await authClient.customer.orders.list({});
98
+
99
+ if (ordersResponse.data) {
100
+ setOrders(ordersResponse.data as unknown as OrdersResponse);
101
+ } else {
102
+ console.log("No orders found or customer not created yet");
103
+ setOrders(null);
104
+ }
105
+ } catch (orderError) {
106
+ console.log(
107
+ "Orders fetch failed - customer may not exist in Polar yet:",
108
+ orderError,
109
+ );
110
+ setOrders(null);
111
+ }
112
+
113
+ try {
114
+ const { data: customerState } = await authClient.customer.state();
115
+ console.log("customerState", customerState);
116
+ } catch (customerError) {
117
+ console.log("Customer state fetch failed:", customerError);
118
+ }
119
+ } catch (error) {
120
+ console.error("Error fetching data:", error);
121
+ } finally {
122
+ setLoading(false);
123
+ }
124
+ };
125
+
126
+ fetchData();
127
+ }, [organizations]);
128
+
129
+ const handleTabChange = (value: string) => {
130
+ setCurrentTab(value);
131
+ const url = new URL(window.location.href);
132
+ url.searchParams.set("tab", value);
133
+ router.replace(url.pathname + url.search, { scroll: false });
134
+ };
135
+
136
+ const handleUpdateProfile = async () => {
137
+ try {
138
+ await authClient.updateUser({
139
+ name,
140
+ });
141
+ toast.success("Profile updated successfully");
142
+ } catch {
143
+ toast.error("Failed to update profile");
144
+ }
145
+ };
146
+
147
+ const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
148
+ const file = e.target.files?.[0];
149
+ if (file) {
150
+ setProfileImage(file);
151
+ const reader = new FileReader();
152
+ reader.onloadend = () => {
153
+ setImagePreview(reader.result as string);
154
+ };
155
+ reader.readAsDataURL(file);
156
+ }
157
+ };
158
+
159
+ const handleUploadProfilePicture = async () => {
160
+ if (!profileImage) return;
161
+
162
+ setUploadingImage(true);
163
+ try {
164
+ const formData = new FormData();
165
+ formData.append("file", profileImage);
166
+
167
+ // Upload to your R2 storage endpoint
168
+ const response = await fetch("/api/upload-image", {
169
+ method: "POST",
170
+ body: formData,
171
+ });
172
+
173
+ if (response.ok) {
174
+ const { url } = await response.json();
175
+
176
+ // Update user profile with new image URL
177
+ await authClient.updateUser({
178
+ name,
179
+ image: url,
180
+ });
181
+
182
+ setUser((prev) => (prev ? { ...prev, image: url } : null));
183
+ setImagePreview(null);
184
+ setProfileImage(null);
185
+ toast.success("Profile picture updated successfully");
186
+ } else {
187
+ throw new Error("Upload failed");
188
+ }
189
+ } catch {
190
+ toast.error("Failed to upload profile picture");
191
+ } finally {
192
+ setUploadingImage(false);
193
+ }
194
+ };
195
+ if (loading) {
196
+ return (
197
+ <div className="flex flex-col gap-6 p-6">
198
+ {/* Header Skeleton */}
199
+ <div>
200
+ <Skeleton className="h-9 w-32 mb-2 bg-gray-200 dark:bg-gray-800" />
201
+ <Skeleton className="h-5 w-80 bg-gray-200 dark:bg-gray-800" />
202
+ </div>
203
+
204
+ {/* Tabs Skeleton */}
205
+ <div className="w-full max-w-4xl">
206
+ <div className="flex space-x-1 mb-6">
207
+ <Skeleton className="h-10 w-20 bg-gray-200 dark:bg-gray-800" />
208
+ <Skeleton className="h-10 w-28 bg-gray-200 dark:bg-gray-800" />
209
+ <Skeleton className="h-10 w-16 bg-gray-200 dark:bg-gray-800" />
210
+ </div>
211
+
212
+ <div className="space-y-6">
213
+ {/* Profile Information Card Skeleton */}
214
+ <Card>
215
+ <CardHeader>
216
+ <div className="flex items-center gap-2">
217
+ <Skeleton className="h-5 w-5 rounded bg-gray-200 dark:bg-gray-800" />
218
+ <Skeleton className="h-6 w-40 bg-gray-200 dark:bg-gray-800" />
219
+ </div>
220
+ <Skeleton className="h-4 w-72 bg-gray-200 dark:bg-gray-800" />
221
+ </CardHeader>
222
+ <CardContent className="space-y-6">
223
+ <div className="flex items-center gap-4">
224
+ <Skeleton className="h-20 w-20 rounded-full bg-gray-200 dark:bg-gray-800" />
225
+ <div className="space-y-2">
226
+ <div className="flex gap-2">
227
+ <Skeleton className="h-8 w-24 bg-gray-200 dark:bg-gray-800" />
228
+ <Skeleton className="h-8 w-12 bg-gray-200 dark:bg-gray-800" />
229
+ <Skeleton className="h-8 w-16 bg-gray-200 dark:bg-gray-800" />
230
+ </div>
231
+ <Skeleton className="h-4 w-48 bg-gray-200 dark:bg-gray-800" />
232
+ </div>
233
+ </div>
234
+
235
+ <div className="grid grid-cols-2 gap-4">
236
+ <div className="space-y-2">
237
+ <Skeleton className="h-4 w-20 bg-gray-200 dark:bg-gray-800" />
238
+ <Skeleton className="h-10 w-full bg-gray-200 dark:bg-gray-800" />
239
+ </div>
240
+ <div className="space-y-2">
241
+ <Skeleton className="h-4 w-12 bg-gray-200 dark:bg-gray-800" />
242
+ <Skeleton className="h-10 w-full bg-gray-200 dark:bg-gray-800" />
243
+ </div>
244
+ </div>
245
+
246
+ <Skeleton className="h-10 w-28 bg-gray-200 dark:bg-gray-800" />
247
+ </CardContent>
248
+ </Card>
249
+
250
+ {/* Change Password Card Skeleton */}
251
+ <Card>
252
+ <CardHeader>
253
+ <Skeleton className="h-6 w-36 bg-gray-200 dark:bg-gray-800" />
254
+ <Skeleton className="h-4 w-64 bg-gray-200 dark:bg-gray-800" />
255
+ </CardHeader>
256
+ <CardContent className="space-y-4">
257
+ <div className="space-y-2">
258
+ <Skeleton className="h-4 w-32 bg-gray-200 dark:bg-gray-800" />
259
+ <Skeleton className="h-10 w-full bg-gray-200 dark:bg-gray-800" />
260
+ </div>
261
+ <div className="space-y-2">
262
+ <Skeleton className="h-4 w-28 bg-gray-200 dark:bg-gray-800" />
263
+ <Skeleton className="h-10 w-full bg-gray-200 dark:bg-gray-800" />
264
+ </div>
265
+ <div className="space-y-2">
266
+ <Skeleton className="h-4 w-40 bg-gray-200 dark:bg-gray-800" />
267
+ <Skeleton className="h-10 w-full bg-gray-200 dark:bg-gray-800" />
268
+ </div>
269
+ <Skeleton className="h-10 w-32 bg-gray-200 dark:bg-gray-800" />
270
+ </CardContent>
271
+ </Card>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+
278
+ return (
279
+ <div className="flex flex-col gap-6 p-6">
280
+ {/* Header */}
281
+ <div>
282
+ <h1 className="text-3xl font-semibold tracking-tight">Settings</h1>
283
+ <p className="text-muted-foreground mt-2">
284
+ Manage your account settings and preferences
285
+ </p>
286
+ </div>
287
+
288
+ <Tabs
289
+ value={currentTab}
290
+ onValueChange={handleTabChange}
291
+ className="w-full max-w-4xl"
292
+ >
293
+ <TabsList>
294
+ <TabsTrigger value="profile">Profile</TabsTrigger>
295
+ <TabsTrigger value="billing">Billing</TabsTrigger>
296
+ </TabsList>
297
+
298
+ <TabsContent value="profile" className="space-y-6">
299
+ {/* Profile Information */}
300
+ <Card>
301
+ <CardHeader>
302
+ <CardTitle className="flex items-center gap-2">
303
+ <Settings2 className="h-5 w-5" />
304
+ Profile Information
305
+ </CardTitle>
306
+ <CardDescription>
307
+ Update your personal information and profile settings
308
+ </CardDescription>
309
+ </CardHeader>
310
+ <CardContent className="space-y-6">
311
+ <div className="flex items-center gap-4">
312
+ <Avatar className="h-20 w-20">
313
+ <AvatarImage src={imagePreview || user?.image || ""} />
314
+ <AvatarFallback>
315
+ {name
316
+ .split(" ")
317
+ .map((n) => n[0])
318
+ .join("")}
319
+ </AvatarFallback>
320
+ </Avatar>
321
+ <div className="space-y-2">
322
+ <div className="flex gap-2">
323
+ <Button
324
+ variant="outline"
325
+ size="sm"
326
+ onClick={() =>
327
+ document.getElementById("profile-image-input")?.click()
328
+ }
329
+ disabled={uploadingImage}
330
+ >
331
+ {uploadingImage ? "Uploading..." : "Change Photo"}
332
+ </Button>
333
+ {profileImage && (
334
+ <Button
335
+ size="sm"
336
+ onClick={handleUploadProfilePicture}
337
+ disabled={uploadingImage}
338
+ >
339
+ Save
340
+ </Button>
341
+ )}
342
+ {imagePreview && (
343
+ <Button
344
+ variant="outline"
345
+ size="sm"
346
+ onClick={() => {
347
+ setImagePreview(null);
348
+ setProfileImage(null);
349
+ }}
350
+ >
351
+ Cancel
352
+ </Button>
353
+ )}
354
+ </div>
355
+ <input
356
+ id="profile-image-input"
357
+ type="file"
358
+ accept="image/*"
359
+ onChange={handleImageChange}
360
+ className="hidden"
361
+ />
362
+ <p className="text-sm text-muted-foreground">
363
+ JPG, GIF or PNG. 1MB max.
364
+ </p>
365
+ </div>
366
+ </div>
367
+
368
+ <div className="grid grid-cols-2 gap-4">
369
+ <div className="space-y-2">
370
+ <Label htmlFor="name">Full Name</Label>
371
+ <Input
372
+ id="name"
373
+ value={name}
374
+ onChange={(e) => setName(e.target.value)}
375
+ placeholder="Enter your full name"
376
+ />
377
+ </div>
378
+ <div className="space-y-2">
379
+ <Label htmlFor="email">Email</Label>
380
+ <Input
381
+ id="email"
382
+ type="email"
383
+ value={email}
384
+ onChange={(e) => setEmail(e.target.value)}
385
+ placeholder="Enter your email"
386
+ disabled
387
+ />
388
+ </div>
389
+ </div>
390
+
391
+ <Button onClick={handleUpdateProfile}>Save Changes</Button>
392
+ </CardContent>
393
+ </Card>
394
+
395
+ {/* Delete Account */}
396
+ <Card className="border-destructive">
397
+ <CardHeader>
398
+ <CardTitle className="text-destructive">Danger Zone</CardTitle>
399
+ <CardDescription>
400
+ Irreversible actions that affect your account
401
+ </CardDescription>
402
+ </CardHeader>
403
+ <CardContent>
404
+ <div className="flex items-center justify-between p-4 bg-destructive/10 rounded-lg">
405
+ <div>
406
+ <h4 className="font-medium">Delete Account</h4>
407
+ <p className="text-sm text-muted-foreground mt-1">
408
+ Permanently delete your account and all associated data. This action cannot be undone.
409
+ </p>
410
+ </div>
411
+ <Button
412
+ variant="destructive"
413
+ onClick={async () => {
414
+ if (
415
+ window.confirm(
416
+ "Are you sure you want to delete your account? This action cannot be undone and all your data will be permanently deleted."
417
+ )
418
+ ) {
419
+ try {
420
+ // First sign out
421
+ await authClient.signOut();
422
+ // Then redirect to home
423
+ router.push("/");
424
+ toast.success("Account deleted successfully");
425
+ } catch (error) {
426
+ console.error("Failed to delete account:", error);
427
+ toast.error("Failed to delete account. Please try again or contact support.");
428
+ }
429
+ }
430
+ }}
431
+ >
432
+ Delete Account
433
+ </Button>
434
+ </div>
435
+ </CardContent>
436
+ </Card>
437
+ </TabsContent>
438
+
439
+ <TabsContent value="billing" className="space-y-6">
440
+ <div className="space-y-4 mt-2">
441
+ <div className="flex justify-between items-center">
442
+ <div>
443
+ <h3 className="text-lg font-medium">Billing History</h3>
444
+ <p className="text-sm text-muted-foreground">
445
+ View your past and upcoming invoices
446
+ </p>
447
+ </div>
448
+ <Button
449
+ variant="outline"
450
+ onClick={async () => {
451
+ try {
452
+ await authClient.customer.portal();
453
+ } catch (error) {
454
+ console.error("Failed to open customer portal:", error);
455
+ // You could add a toast notification here
456
+ }
457
+ }}
458
+ disabled={orders === null}
459
+ >
460
+ <ExternalLink className="h-4 w-4 mr-2" />
461
+ Manage Subscription
462
+ </Button>
463
+ </div>
464
+ {orders?.result?.items && orders.result.items.length > 0 ? (
465
+ <div className="space-y-4">
466
+ {(orders.result.items || []).map((order) => (
467
+ <Card key={order.id} className="overflow-hidden">
468
+ <CardContent className="px-4">
469
+ <div className="flex flex-col gap-3">
470
+ {/* Header Row */}
471
+ <div className="flex items-start justify-between gap-3">
472
+ <div>
473
+ <div className="flex justify-center gap-2">
474
+ <h4 className="font-medium text-base">
475
+ {order.product?.name || "Subscription"}
476
+ </h4>
477
+ <div className="flex items-center gap-2">
478
+ {order.subscription?.status === "paid" ? (
479
+ <Badge className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 text-xs">
480
+ Paid
481
+ </Badge>
482
+ ) : order.subscription?.status ===
483
+ "canceled" ? (
484
+ <Badge
485
+ variant="destructive"
486
+ className="text-xs"
487
+ >
488
+ Canceled
489
+ </Badge>
490
+ ) : order.subscription?.status ===
491
+ "refunded" ? (
492
+ <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 text-xs">
493
+ Refunded
494
+ </Badge>
495
+ ) : (
496
+ <Badge variant="outline" className="text-xs">
497
+ {order.subscription?.status}
498
+ </Badge>
499
+ )}
500
+
501
+ {order.subscription?.status === "canceled" && (
502
+ <span className="text-xs text-muted-foreground">
503
+ • Canceled on{" "}
504
+ {order.subscription.endedAt
505
+ ? new Date(
506
+ order.subscription.endedAt,
507
+ ).toLocaleDateString("en-US", {
508
+ month: "short",
509
+ day: "numeric",
510
+ })
511
+ : "N/A"}
512
+ </span>
513
+ )}
514
+ </div>
515
+ </div>
516
+ <div className="text-sm text-muted-foreground">
517
+ {new Date(order.createdAt).toLocaleDateString(
518
+ "en-US",
519
+ {
520
+ year: "numeric",
521
+ month: "short",
522
+ day: "numeric",
523
+ },
524
+ )}
525
+ </div>
526
+ </div>
527
+
528
+ <div className="text-right">
529
+ <div className="font-medium text-base">
530
+ ${(order.totalAmount / 100).toFixed(2)}
531
+ </div>
532
+ <div className="text-xs text-muted-foreground">
533
+ {order.currency?.toUpperCase()}
534
+ </div>
535
+ </div>
536
+ </div>
537
+
538
+ {/* Order Items */}
539
+ {order.items?.length > 0 && (
540
+ <div className="mt-2 pt-3 border-t">
541
+ <ul className="space-y-1.5 text-sm">
542
+ {order.items.map((item, index: number) => (
543
+ <li
544
+ key={`${order.id}-${item.label}-${index}`}
545
+ className="flex justify-between"
546
+ >
547
+ <span className="text-muted-foreground truncate max-w-[200px]">
548
+ {item.label}
549
+ </span>
550
+ <span className="font-medium">
551
+ ${(item.amount / 100).toFixed(2)}
552
+ </span>
553
+ </li>
554
+ ))}
555
+ </ul>
556
+ </div>
557
+ )}
558
+ </div>
559
+ </CardContent>
560
+ </Card>
561
+ ))}
562
+ </div>
563
+ ) : (
564
+ <Card>
565
+ <CardContent className="p-8 text-center">
566
+ <div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
567
+ <svg
568
+ xmlns="http://www.w3.org/2000/svg"
569
+ fill="none"
570
+ stroke="currentColor"
571
+ strokeLinecap="round"
572
+ strokeLinejoin="round"
573
+ strokeWidth="1.5"
574
+ className="h-10 w-10 text-muted-foreground mb-4"
575
+ viewBox="0 0 24 24"
576
+ >
577
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
578
+ </svg>
579
+ <h3 className="mt-4 text-lg font-semibold">
580
+ No orders found
581
+ </h3>
582
+ <p className="mb-4 mt-2 text-sm text-muted-foreground">
583
+ {orders === null
584
+ ? "Unable to load billing history. This may be because your account is not yet set up for billing."
585
+ : "You don&apos;t have any orders yet. Your billing history will appear here."}
586
+ </p>
587
+ </div>
588
+ </CardContent>
589
+ </Card>
590
+ )}
591
+ </div>
592
+ </TabsContent>
593
+ </Tabs>
594
+ </div>
595
+ );
596
+ }
597
+
598
+ export default function SettingsPage() {
599
+ return (
600
+ <Suspense
601
+ fallback={
602
+ <div className="flex flex-col gap-6 p-6">
603
+ <div>
604
+ <div className="h-9 w-32 mb-2 bg-gray-200 dark:bg-gray-800 animate-pulse rounded-md" />
605
+ <div className="h-5 w-80 bg-gray-200 dark:bg-gray-800 animate-pulse rounded-md" />
606
+ </div>
607
+ </div>
608
+ }
609
+ >
610
+ <SettingsContent />
611
+ </Suspense>
612
+ );
613
+ }