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.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1366 -0
- package/docs-template/README.md +255 -0
- package/docs-template/[slug]/[subslug]/page.tsx +1242 -0
- package/docs-template/[slug]/page.tsx +422 -0
- package/docs-template/api/page.tsx +47 -0
- package/docs-template/components/docs/docs-category-page.tsx +162 -0
- package/docs-template/components/docs/docs-code-card.tsx +135 -0
- package/docs-template/components/docs/docs-header.tsx +69 -0
- package/docs-template/components/docs/docs-nav.ts +95 -0
- package/docs-template/components/docs/docs-sidebar.tsx +112 -0
- package/docs-template/components/docs/docs-toc.tsx +38 -0
- package/docs-template/components/ui/badge.tsx +47 -0
- package/docs-template/components/ui/button.tsx +60 -0
- package/docs-template/components/ui/card.tsx +93 -0
- package/docs-template/components/ui/sheet.tsx +140 -0
- package/docs-template/documentation/page.tsx +80 -0
- package/docs-template/layout.tsx +27 -0
- package/docs-template/lib/utils.ts +7 -0
- package/docs-template/page.tsx +360 -0
- package/package.json +66 -0
- package/template/.env.example +45 -0
- package/template/README.md +239 -0
- package/template/app/api/auth/[...all]/route.ts +4 -0
- package/template/app/api/chat/route.ts +16 -0
- package/template/app/api/subscription/route.ts +25 -0
- package/template/app/api/upload-image/route.ts +64 -0
- package/template/app/blog/[slug]/page.tsx +314 -0
- package/template/app/blog/page.tsx +107 -0
- package/template/app/dashboard/_components/chart-interactive.tsx +289 -0
- package/template/app/dashboard/_components/chatbot.tsx +39 -0
- package/template/app/dashboard/_components/mode-toggle.tsx +46 -0
- package/template/app/dashboard/_components/navbar.tsx +84 -0
- package/template/app/dashboard/_components/section-cards.tsx +102 -0
- package/template/app/dashboard/_components/sidebar.tsx +90 -0
- package/template/app/dashboard/_components/subscribe-button.tsx +49 -0
- package/template/app/dashboard/billing/page.tsx +277 -0
- package/template/app/dashboard/chat/page.tsx +73 -0
- package/template/app/dashboard/cli/page.tsx +260 -0
- package/template/app/dashboard/layout.tsx +24 -0
- package/template/app/dashboard/page.tsx +216 -0
- package/template/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/template/app/dashboard/payment/page.tsx +126 -0
- package/template/app/dashboard/settings/page.tsx +613 -0
- package/template/app/dashboard/upload/page.tsx +324 -0
- package/template/app/error.tsx +78 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +126 -0
- package/template/app/layout.tsx +135 -0
- package/template/app/not-found.tsx +45 -0
- package/template/app/page.tsx +28 -0
- package/template/app/pricing/_component/pricing-table.tsx +276 -0
- package/template/app/pricing/page.tsx +23 -0
- package/template/app/privacy-policy/page.tsx +280 -0
- package/template/app/robots.txt +12 -0
- package/template/app/sign-in/page.tsx +228 -0
- package/template/app/sign-up/page.tsx +243 -0
- package/template/app/sitemap.ts +62 -0
- package/template/app/success/page.tsx +123 -0
- package/template/app/terms-of-service/page.tsx +212 -0
- package/template/auth-schema.ts +47 -0
- package/template/components/homepage/cli-workflow-section.tsx +138 -0
- package/template/components/homepage/features-section.tsx +150 -0
- package/template/components/homepage/footer.tsx +53 -0
- package/template/components/homepage/hero-section.tsx +112 -0
- package/template/components/homepage/integrations.tsx +124 -0
- package/template/components/homepage/navigation.tsx +116 -0
- package/template/components/homepage/news-section.tsx +82 -0
- package/template/components/homepage/testimonials-section.tsx +34 -0
- package/template/components/logos/BetterAuth.tsx +21 -0
- package/template/components/logos/NeonPostgres.tsx +41 -0
- package/template/components/logos/Nextjs.tsx +72 -0
- package/template/components/logos/Polar.tsx +7 -0
- package/template/components/logos/TailwindCSS.tsx +27 -0
- package/template/components/logos/index.ts +6 -0
- package/template/components/logos/shadcnui.tsx +8 -0
- package/template/components/provider.tsx +8 -0
- package/template/components/ui/avatar.tsx +53 -0
- package/template/components/ui/badge.tsx +46 -0
- package/template/components/ui/button.tsx +59 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/chart.tsx +353 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/dialog.tsx +135 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/resizable.tsx +56 -0
- package/template/components/ui/select.tsx +185 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +139 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/sonner.tsx +25 -0
- package/template/components/ui/switch.tsx +31 -0
- package/template/components/ui/tabs.tsx +66 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +73 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +61 -0
- package/template/components/user-profile.tsx +139 -0
- package/template/components.json +21 -0
- package/template/db/drizzle.ts +14 -0
- package/template/db/migrations/0000_worried_rawhide_kid.sql +77 -0
- package/template/db/migrations/meta/0000_snapshot.json +494 -0
- package/template/db/migrations/meta/_journal.json +13 -0
- package/template/db/schema.ts +85 -0
- package/template/drizzle.config.ts +13 -0
- package/template/emails/components/layout.tsx +181 -0
- package/template/emails/password-reset.tsx +67 -0
- package/template/emails/payment-failed.tsx +167 -0
- package/template/emails/subscription-confirmation.tsx +129 -0
- package/template/emails/welcome.tsx +100 -0
- package/template/eslint.config.mjs +16 -0
- package/template/hooks/use-mobile.ts +21 -0
- package/template/lib/auth-client.ts +8 -0
- package/template/lib/auth.ts +276 -0
- package/template/lib/email.ts +118 -0
- package/template/lib/polar-products.ts +49 -0
- package/template/lib/subscription.ts +148 -0
- package/template/lib/upload-image.ts +28 -0
- package/template/lib/utils.ts +6 -0
- package/template/middleware.ts +30 -0
- package/template/next-env.d.ts +5 -0
- package/template/next.config.ts +27 -0
- package/template/package.json +99 -0
- package/template/postcss.config.mjs +5 -0
- package/template/public/add.png +0 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/iphone.png +0 -0
- package/template/public/logo.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/polar-sh.svg +1 -0
- package/template/public/shadcn-ui.svg +1 -0
- package/template/public/site.webmanifest +21 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/tailwind.config.ts +89 -0
- package/template/template.config.json +138 -0
- 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'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
|
+
}
|