@codaijs/keel 0.2.3 → 0.2.4
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/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { useParams, useNavigate } from "react-router";
|
|
3
|
-
import { fetchUser, updateUser, deleteUser, type AdminUser } from "@/hooks/useAdmin.js";
|
|
4
|
-
|
|
5
|
-
export default function UserDetail() {
|
|
6
|
-
const { id } = useParams<{ id: string }>();
|
|
7
|
-
const navigate = useNavigate();
|
|
8
|
-
|
|
9
|
-
const [user, setUser] = useState<AdminUser | null>(null);
|
|
10
|
-
const [activeSessions, setActiveSessions] = useState(0);
|
|
11
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
-
const [error, setError] = useState<string | null>(null);
|
|
13
|
-
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
14
|
-
|
|
15
|
-
const load = useCallback(async () => {
|
|
16
|
-
if (!id) return;
|
|
17
|
-
setIsLoading(true);
|
|
18
|
-
setError(null);
|
|
19
|
-
try {
|
|
20
|
-
const data = await fetchUser(id);
|
|
21
|
-
setUser(data.user);
|
|
22
|
-
setActiveSessions(data.activeSessions);
|
|
23
|
-
} catch (err) {
|
|
24
|
-
setError(err instanceof Error ? err.message : "Failed to load user");
|
|
25
|
-
} finally {
|
|
26
|
-
setIsLoading(false);
|
|
27
|
-
}
|
|
28
|
-
}, [id]);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
load();
|
|
32
|
-
}, [load]);
|
|
33
|
-
|
|
34
|
-
const handleVerifyEmail = async () => {
|
|
35
|
-
if (!user) return;
|
|
36
|
-
setActionLoading("verify");
|
|
37
|
-
try {
|
|
38
|
-
const result = await updateUser(user.id, { emailVerified: true });
|
|
39
|
-
setUser(result.user);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
setError(err instanceof Error ? err.message : "Failed to verify email");
|
|
42
|
-
} finally {
|
|
43
|
-
setActionLoading(null);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const handleDelete = async () => {
|
|
48
|
-
if (!user) return;
|
|
49
|
-
const confirmed = window.confirm(
|
|
50
|
-
`Are you sure you want to delete ${user.name} (${user.email})? This action cannot be undone.`,
|
|
51
|
-
);
|
|
52
|
-
if (!confirmed) return;
|
|
53
|
-
|
|
54
|
-
setActionLoading("delete");
|
|
55
|
-
try {
|
|
56
|
-
await deleteUser(user.id);
|
|
57
|
-
navigate("/admin");
|
|
58
|
-
} catch (err) {
|
|
59
|
-
setError(err instanceof Error ? err.message : "Failed to delete user");
|
|
60
|
-
setActionLoading(null);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (isLoading) {
|
|
65
|
-
return (
|
|
66
|
-
<div className="flex min-h-[50vh] items-center justify-center">
|
|
67
|
-
<div className="h-8 w-8 animate-spin rounded-full border-2 border-keel-gray-800 border-t-keel-blue" />
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (error || !user) {
|
|
73
|
-
return (
|
|
74
|
-
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
75
|
-
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-6 text-center">
|
|
76
|
-
<p className="text-sm text-red-400">{error ?? "User not found"}</p>
|
|
77
|
-
<button
|
|
78
|
-
onClick={() => navigate("/admin")}
|
|
79
|
-
className="mt-4 text-sm font-medium text-keel-blue hover:underline"
|
|
80
|
-
>
|
|
81
|
-
Back to dashboard
|
|
82
|
-
</button>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
-
{/* Back button */}
|
|
91
|
-
<button
|
|
92
|
-
onClick={() => navigate("/admin")}
|
|
93
|
-
className="mb-6 flex items-center gap-1.5 text-sm font-medium text-keel-gray-400 transition-colors hover:text-white"
|
|
94
|
-
>
|
|
95
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
96
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
97
|
-
</svg>
|
|
98
|
-
Back to dashboard
|
|
99
|
-
</button>
|
|
100
|
-
|
|
101
|
-
{/* User info card */}
|
|
102
|
-
<div className="rounded-xl border border-keel-gray-800 bg-keel-gray-900 p-6">
|
|
103
|
-
<div className="flex items-start gap-4">
|
|
104
|
-
{user.image ? (
|
|
105
|
-
<img
|
|
106
|
-
src={user.image}
|
|
107
|
-
alt={user.name}
|
|
108
|
-
className="h-16 w-16 rounded-full object-cover"
|
|
109
|
-
/>
|
|
110
|
-
) : (
|
|
111
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-keel-blue/20 text-xl font-bold text-keel-blue">
|
|
112
|
-
{user.name.charAt(0).toUpperCase()}
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
115
|
-
|
|
116
|
-
<div className="min-w-0 flex-1">
|
|
117
|
-
<h1 className="text-xl font-bold text-white">{user.name}</h1>
|
|
118
|
-
<p className="text-sm text-keel-gray-400">{user.email}</p>
|
|
119
|
-
|
|
120
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
121
|
-
{user.emailVerified ? (
|
|
122
|
-
<span className="inline-flex items-center rounded-full bg-green-500/10 px-2.5 py-0.5 text-xs font-medium text-green-400">
|
|
123
|
-
Email Verified
|
|
124
|
-
</span>
|
|
125
|
-
) : (
|
|
126
|
-
<span className="inline-flex items-center rounded-full bg-yellow-500/10 px-2.5 py-0.5 text-xs font-medium text-yellow-400">
|
|
127
|
-
Email Not Verified
|
|
128
|
-
</span>
|
|
129
|
-
)}
|
|
130
|
-
<span className="inline-flex items-center rounded-full bg-keel-blue/10 px-2.5 py-0.5 text-xs font-medium text-keel-blue">
|
|
131
|
-
{activeSessions} active session{activeSessions !== 1 ? "s" : ""}
|
|
132
|
-
</span>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
{/* Details */}
|
|
138
|
-
<div className="mt-6 grid grid-cols-1 gap-4 border-t border-keel-gray-800 pt-6 sm:grid-cols-2">
|
|
139
|
-
<div>
|
|
140
|
-
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
141
|
-
User ID
|
|
142
|
-
</p>
|
|
143
|
-
<p className="mt-1 font-mono text-sm text-keel-gray-200">{user.id}</p>
|
|
144
|
-
</div>
|
|
145
|
-
<div>
|
|
146
|
-
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
147
|
-
Member Since
|
|
148
|
-
</p>
|
|
149
|
-
<p className="mt-1 text-sm text-keel-gray-200">
|
|
150
|
-
{new Date(user.createdAt).toLocaleDateString(undefined, {
|
|
151
|
-
year: "numeric",
|
|
152
|
-
month: "long",
|
|
153
|
-
day: "numeric",
|
|
154
|
-
})}
|
|
155
|
-
</p>
|
|
156
|
-
</div>
|
|
157
|
-
<div>
|
|
158
|
-
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
159
|
-
Last Updated
|
|
160
|
-
</p>
|
|
161
|
-
<p className="mt-1 text-sm text-keel-gray-200">
|
|
162
|
-
{new Date(user.updatedAt).toLocaleDateString(undefined, {
|
|
163
|
-
year: "numeric",
|
|
164
|
-
month: "long",
|
|
165
|
-
day: "numeric",
|
|
166
|
-
hour: "2-digit",
|
|
167
|
-
minute: "2-digit",
|
|
168
|
-
})}
|
|
169
|
-
</p>
|
|
170
|
-
</div>
|
|
171
|
-
<div>
|
|
172
|
-
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
173
|
-
Profile Image
|
|
174
|
-
</p>
|
|
175
|
-
<p className="mt-1 text-sm text-keel-gray-200">
|
|
176
|
-
{user.image ? "Custom image set" : "No image"}
|
|
177
|
-
</p>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
{/* Actions */}
|
|
182
|
-
<div className="mt-6 flex flex-wrap gap-3 border-t border-keel-gray-800 pt-6">
|
|
183
|
-
{!user.emailVerified && (
|
|
184
|
-
<button
|
|
185
|
-
onClick={handleVerifyEmail}
|
|
186
|
-
disabled={actionLoading === "verify"}
|
|
187
|
-
className="rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
188
|
-
>
|
|
189
|
-
{actionLoading === "verify" ? "Verifying..." : "Verify Email"}
|
|
190
|
-
</button>
|
|
191
|
-
)}
|
|
192
|
-
<button
|
|
193
|
-
onClick={handleDelete}
|
|
194
|
-
disabled={!!actionLoading}
|
|
195
|
-
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
196
|
-
>
|
|
197
|
-
{actionLoading === "delete" ? "Deleting..." : "Delete User"}
|
|
198
|
-
</button>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { useParams, useNavigate } from "react-router";
|
|
3
|
+
import { fetchUser, updateUser, deleteUser, type AdminUser } from "@/hooks/useAdmin.js";
|
|
4
|
+
|
|
5
|
+
export default function UserDetail() {
|
|
6
|
+
const { id } = useParams<{ id: string }>();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
|
|
9
|
+
const [user, setUser] = useState<AdminUser | null>(null);
|
|
10
|
+
const [activeSessions, setActiveSessions] = useState(0);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const load = useCallback(async () => {
|
|
16
|
+
if (!id) return;
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const data = await fetchUser(id);
|
|
21
|
+
setUser(data.user);
|
|
22
|
+
setActiveSessions(data.activeSessions);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
setError(err instanceof Error ? err.message : "Failed to load user");
|
|
25
|
+
} finally {
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, [id]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
load();
|
|
32
|
+
}, [load]);
|
|
33
|
+
|
|
34
|
+
const handleVerifyEmail = async () => {
|
|
35
|
+
if (!user) return;
|
|
36
|
+
setActionLoading("verify");
|
|
37
|
+
try {
|
|
38
|
+
const result = await updateUser(user.id, { emailVerified: true });
|
|
39
|
+
setUser(result.user);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err instanceof Error ? err.message : "Failed to verify email");
|
|
42
|
+
} finally {
|
|
43
|
+
setActionLoading(null);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleDelete = async () => {
|
|
48
|
+
if (!user) return;
|
|
49
|
+
const confirmed = window.confirm(
|
|
50
|
+
`Are you sure you want to delete ${user.name} (${user.email})? This action cannot be undone.`,
|
|
51
|
+
);
|
|
52
|
+
if (!confirmed) return;
|
|
53
|
+
|
|
54
|
+
setActionLoading("delete");
|
|
55
|
+
try {
|
|
56
|
+
await deleteUser(user.id);
|
|
57
|
+
navigate("/admin");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError(err instanceof Error ? err.message : "Failed to delete user");
|
|
60
|
+
setActionLoading(null);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isLoading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-h-[50vh] items-center justify-center">
|
|
67
|
+
<div className="h-8 w-8 animate-spin rounded-full border-2 border-keel-gray-800 border-t-keel-blue" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error || !user) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
75
|
+
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-6 text-center">
|
|
76
|
+
<p className="text-sm text-red-400">{error ?? "User not found"}</p>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => navigate("/admin")}
|
|
79
|
+
className="mt-4 text-sm font-medium text-keel-blue hover:underline"
|
|
80
|
+
>
|
|
81
|
+
Back to dashboard
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
+
{/* Back button */}
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => navigate("/admin")}
|
|
93
|
+
className="mb-6 flex items-center gap-1.5 text-sm font-medium text-keel-gray-400 transition-colors hover:text-white"
|
|
94
|
+
>
|
|
95
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
96
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
97
|
+
</svg>
|
|
98
|
+
Back to dashboard
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
{/* User info card */}
|
|
102
|
+
<div className="rounded-xl border border-keel-gray-800 bg-keel-gray-900 p-6">
|
|
103
|
+
<div className="flex items-start gap-4">
|
|
104
|
+
{user.image ? (
|
|
105
|
+
<img
|
|
106
|
+
src={user.image}
|
|
107
|
+
alt={user.name}
|
|
108
|
+
className="h-16 w-16 rounded-full object-cover"
|
|
109
|
+
/>
|
|
110
|
+
) : (
|
|
111
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-keel-blue/20 text-xl font-bold text-keel-blue">
|
|
112
|
+
{user.name.charAt(0).toUpperCase()}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<div className="min-w-0 flex-1">
|
|
117
|
+
<h1 className="text-xl font-bold text-white">{user.name}</h1>
|
|
118
|
+
<p className="text-sm text-keel-gray-400">{user.email}</p>
|
|
119
|
+
|
|
120
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
121
|
+
{user.emailVerified ? (
|
|
122
|
+
<span className="inline-flex items-center rounded-full bg-green-500/10 px-2.5 py-0.5 text-xs font-medium text-green-400">
|
|
123
|
+
Email Verified
|
|
124
|
+
</span>
|
|
125
|
+
) : (
|
|
126
|
+
<span className="inline-flex items-center rounded-full bg-yellow-500/10 px-2.5 py-0.5 text-xs font-medium text-yellow-400">
|
|
127
|
+
Email Not Verified
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
<span className="inline-flex items-center rounded-full bg-keel-blue/10 px-2.5 py-0.5 text-xs font-medium text-keel-blue">
|
|
131
|
+
{activeSessions} active session{activeSessions !== 1 ? "s" : ""}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Details */}
|
|
138
|
+
<div className="mt-6 grid grid-cols-1 gap-4 border-t border-keel-gray-800 pt-6 sm:grid-cols-2">
|
|
139
|
+
<div>
|
|
140
|
+
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
141
|
+
User ID
|
|
142
|
+
</p>
|
|
143
|
+
<p className="mt-1 font-mono text-sm text-keel-gray-200">{user.id}</p>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
147
|
+
Member Since
|
|
148
|
+
</p>
|
|
149
|
+
<p className="mt-1 text-sm text-keel-gray-200">
|
|
150
|
+
{new Date(user.createdAt).toLocaleDateString(undefined, {
|
|
151
|
+
year: "numeric",
|
|
152
|
+
month: "long",
|
|
153
|
+
day: "numeric",
|
|
154
|
+
})}
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
159
|
+
Last Updated
|
|
160
|
+
</p>
|
|
161
|
+
<p className="mt-1 text-sm text-keel-gray-200">
|
|
162
|
+
{new Date(user.updatedAt).toLocaleDateString(undefined, {
|
|
163
|
+
year: "numeric",
|
|
164
|
+
month: "long",
|
|
165
|
+
day: "numeric",
|
|
166
|
+
hour: "2-digit",
|
|
167
|
+
minute: "2-digit",
|
|
168
|
+
})}
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
<div>
|
|
172
|
+
<p className="text-xs font-medium uppercase tracking-wider text-keel-gray-400">
|
|
173
|
+
Profile Image
|
|
174
|
+
</p>
|
|
175
|
+
<p className="mt-1 text-sm text-keel-gray-200">
|
|
176
|
+
{user.image ? "Custom image set" : "No image"}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Actions */}
|
|
182
|
+
<div className="mt-6 flex flex-wrap gap-3 border-t border-keel-gray-800 pt-6">
|
|
183
|
+
{!user.emailVerified && (
|
|
184
|
+
<button
|
|
185
|
+
onClick={handleVerifyEmail}
|
|
186
|
+
disabled={actionLoading === "verify"}
|
|
187
|
+
className="rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
188
|
+
>
|
|
189
|
+
{actionLoading === "verify" ? "Verifying..." : "Verify Email"}
|
|
190
|
+
</button>
|
|
191
|
+
)}
|
|
192
|
+
<button
|
|
193
|
+
onClick={handleDelete}
|
|
194
|
+
disabled={!!actionLoading}
|
|
195
|
+
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
196
|
+
>
|
|
197
|
+
{actionLoading === "delete" ? "Deleting..." : "Delete User"}
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|