@xk2800/nextjs-template 0.5.3
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/README.md +339 -0
- package/components/auth/authCard.tsx +92 -0
- package/components/auth/authShell.tsx +46 -0
- package/components/auth/deviceCheck.tsx +52 -0
- package/components/auth/emailPasswordLogin.tsx +136 -0
- package/components/auth/emailPasswordSignup.tsx +178 -0
- package/components/auth/forgotPasswordForm.tsx +66 -0
- package/components/auth/loginErrorRecovery.tsx +117 -0
- package/components/auth/logoutButtons.tsx +26 -0
- package/components/auth/oneTap.tsx +50 -0
- package/components/auth/passkeyLogin.tsx +50 -0
- package/components/auth/resetPasswordForm.tsx +112 -0
- package/components/auth/socialLogin.tsx +51 -0
- package/components/auth/twoFactorVerifyForm.tsx +100 -0
- package/components/dashboard/accountDetailsCard.tsx +58 -0
- package/components/dashboard/activityLogsCard.tsx +141 -0
- package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
- package/components/dashboard/activityLogsSection.tsx +16 -0
- package/components/dashboard/admin/activityLogsSection.tsx +13 -0
- package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
- package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
- package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
- package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
- package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
- package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
- package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
- package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
- package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
- package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
- package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
- package/components/dashboard/admin/adminUsersTable.tsx +596 -0
- package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
- package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
- package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
- package/components/dashboard/admin/newDeviceCard.tsx +35 -0
- package/components/dashboard/admin/newDeviceTable.tsx +71 -0
- package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
- package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
- package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
- package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
- package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
- package/components/dashboard/admin/usersSection.tsx +13 -0
- package/components/dashboard/adminSection.tsx +31 -0
- package/components/dashboard/appSidebar.tsx +170 -0
- package/components/dashboard/dashboardHeader.tsx +85 -0
- package/components/dashboard/heartbeat.tsx +22 -0
- package/components/dashboard/impersonationBanner.tsx +50 -0
- package/components/dashboard/passkeysCard.tsx +187 -0
- package/components/dashboard/profileCard.tsx +42 -0
- package/components/dashboard/sectionErrorFallback.tsx +18 -0
- package/components/dashboard/sessionsCard.tsx +194 -0
- package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
- package/components/dashboard/sessionsSection.tsx +22 -0
- package/components/dashboard/twoFactorCard.tsx +344 -0
- package/components/dashboard/verifyEmailButton.tsx +32 -0
- package/components/email/email-template-2.tsx +44 -0
- package/components/email/email-template-new-device.tsx +80 -0
- package/components/email/email-template-passkey-change.tsx +81 -0
- package/components/email/email-template-reset-password.tsx +56 -0
- package/components/email/email-template-verify-email.tsx +55 -0
- package/components/email/email-template-welcome.tsx +53 -0
- package/components/email/email-template.tsx +33 -0
- package/components/email/send-email-button.tsx +61 -0
- package/components/providers/theme-provider.tsx +9 -0
- package/components/theme/theme-toggle.tsx +53 -0
- package/components/ui/alert-dialog.tsx +141 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/data-table.tsx +302 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +203 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/password-input.tsx +38 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +143 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +57 -0
- package/dist/activity/logger.d.mts +18 -0
- package/dist/activity/logger.d.ts +18 -0
- package/dist/activity/logger.js +289 -0
- package/dist/activity/logger.js.map +1 -0
- package/dist/activity/logger.mjs +7 -0
- package/dist/activity/logger.mjs.map +1 -0
- package/dist/activity/queries.d.mts +98 -0
- package/dist/activity/queries.d.ts +98 -0
- package/dist/activity/queries.js +372 -0
- package/dist/activity/queries.js.map +1 -0
- package/dist/activity/queries.mjs +124 -0
- package/dist/activity/queries.mjs.map +1 -0
- package/dist/admin/queries.d.mts +68 -0
- package/dist/admin/queries.d.ts +68 -0
- package/dist/admin/queries.js +450 -0
- package/dist/admin/queries.js.map +1 -0
- package/dist/admin/queries.mjs +143 -0
- package/dist/admin/queries.mjs.map +1 -0
- package/dist/auth/helpers.d.mts +111 -0
- package/dist/auth/helpers.d.ts +111 -0
- package/dist/auth/helpers.js +1023 -0
- package/dist/auth/helpers.js.map +1 -0
- package/dist/auth/helpers.mjs +67 -0
- package/dist/auth/helpers.mjs.map +1 -0
- package/dist/auth/index.d.mts +6589 -0
- package/dist/auth/index.d.ts +6589 -0
- package/dist/auth/index.js +968 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +10 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth-client/index.d.mts +4864 -0
- package/dist/auth-client/index.d.ts +4864 -0
- package/dist/auth-client/index.js +55 -0
- package/dist/auth-client/index.js.map +1 -0
- package/dist/auth-client/index.mjs +49 -0
- package/dist/auth-client/index.mjs.map +1 -0
- package/dist/chunk-2FW7C3KS.mjs +556 -0
- package/dist/chunk-2FW7C3KS.mjs.map +1 -0
- package/dist/chunk-ENGYO4K7.mjs +189 -0
- package/dist/chunk-ENGYO4K7.mjs.map +1 -0
- package/dist/chunk-GTPTVNY2.mjs +40 -0
- package/dist/chunk-GTPTVNY2.mjs.map +1 -0
- package/dist/chunk-HYKUB3A2.mjs +30 -0
- package/dist/chunk-HYKUB3A2.mjs.map +1 -0
- package/dist/chunk-JIYHBWAU.mjs +79 -0
- package/dist/chunk-JIYHBWAU.mjs.map +1 -0
- package/dist/chunk-K6OJCSJS.mjs +41 -0
- package/dist/chunk-K6OJCSJS.mjs.map +1 -0
- package/dist/chunk-N6ECRCHY.mjs +61 -0
- package/dist/chunk-N6ECRCHY.mjs.map +1 -0
- package/dist/chunk-YN4436PK.mjs +33 -0
- package/dist/chunk-YN4436PK.mjs.map +1 -0
- package/dist/config/env.d.mts +15 -0
- package/dist/config/env.d.ts +15 -0
- package/dist/config/env.js +42 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/env.mjs +4 -0
- package/dist/config/env.mjs.map +1 -0
- package/dist/db/index.d.mts +10 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +252 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/index.mjs +6 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/schema.d.mts +2 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +186 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/schema.mjs +4 -0
- package/dist/db/schema.mjs.map +1 -0
- package/dist/schema-CgkDv4wJ.d.mts +1758 -0
- package/dist/schema-CgkDv4wJ.d.ts +1758 -0
- package/dist/sessions/queries.d.mts +34 -0
- package/dist/sessions/queries.d.ts +34 -0
- package/dist/sessions/queries.js +268 -0
- package/dist/sessions/queries.js.map +1 -0
- package/dist/sessions/queries.mjs +23 -0
- package/dist/sessions/queries.mjs.map +1 -0
- package/dist/settings/queries.d.mts +20 -0
- package/dist/settings/queries.d.ts +20 -0
- package/dist/settings/queries.js +344 -0
- package/dist/settings/queries.js.map +1 -0
- package/dist/settings/queries.mjs +7 -0
- package/dist/settings/queries.mjs.map +1 -0
- package/dist/types/auth/loginSchema.d.mts +8 -0
- package/dist/types/auth/loginSchema.d.ts +8 -0
- package/dist/types/auth/loginSchema.js +18 -0
- package/dist/types/auth/loginSchema.js.map +1 -0
- package/dist/types/auth/loginSchema.mjs +16 -0
- package/dist/types/auth/loginSchema.mjs.map +1 -0
- package/dist/types/auth/signupSchema.d.mts +9 -0
- package/dist/types/auth/signupSchema.d.ts +9 -0
- package/dist/types/auth/signupSchema.js +21 -0
- package/dist/types/auth/signupSchema.js.map +1 -0
- package/dist/types/auth/signupSchema.mjs +19 -0
- package/dist/types/auth/signupSchema.mjs.map +1 -0
- package/dist/users/queries.d.mts +71 -0
- package/dist/users/queries.d.ts +71 -0
- package/dist/users/queries.js +293 -0
- package/dist/users/queries.js.map +1 -0
- package/dist/users/queries.mjs +47 -0
- package/dist/users/queries.mjs.map +1 -0
- package/lib/auth-client.ts +58 -0
- package/lib/cookie-names.ts +13 -0
- package/lib/device-fingerprint.ts +21 -0
- package/lib/formatters.ts +53 -0
- package/lib/recalculate-columns.ts +75 -0
- package/lib/request-info.ts +78 -0
- package/lib/utils.ts +6 -0
- package/package.json +301 -0
- package/styles/theme.css +124 -0
- package/types/auth/loginSchema.ts +11 -0
- package/types/auth/signupSchema.ts +14 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
|
|
5
|
+
import { Button } from "../../ui/button"
|
|
6
|
+
import { DataTable, type Column } from "../../ui/data-table"
|
|
7
|
+
import {
|
|
8
|
+
AlertDialog,
|
|
9
|
+
AlertDialogAction,
|
|
10
|
+
AlertDialogCancel,
|
|
11
|
+
AlertDialogContent,
|
|
12
|
+
AlertDialogDescription,
|
|
13
|
+
AlertDialogFooter,
|
|
14
|
+
AlertDialogHeader,
|
|
15
|
+
AlertDialogTitle,
|
|
16
|
+
} from "../../ui/alert-dialog"
|
|
17
|
+
import { formatDateTime } from "../../../lib/formatters"
|
|
18
|
+
import { useRouter } from "next/navigation"
|
|
19
|
+
import { toast } from "sonner"
|
|
20
|
+
|
|
21
|
+
interface Session {
|
|
22
|
+
id: string
|
|
23
|
+
createdAt: Date
|
|
24
|
+
expiresAt: Date
|
|
25
|
+
ipAddress: string | null
|
|
26
|
+
userAgent: string | null
|
|
27
|
+
deviceLabel: string
|
|
28
|
+
location: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AdminUserSessionsCardProps {
|
|
32
|
+
userId: string
|
|
33
|
+
initialSessions: Session[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseColumns: Column<Session>[] = [
|
|
37
|
+
{
|
|
38
|
+
name: 'createdAt',
|
|
39
|
+
title: 'Created',
|
|
40
|
+
renderer: (session) => <span className="text-sm">{formatDateTime(session.createdAt)}</span>,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'expiresAt',
|
|
44
|
+
title: 'Expires',
|
|
45
|
+
renderer: (session) => <span className="text-sm">{formatDateTime(session.expiresAt)}</span>,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'deviceLabel',
|
|
49
|
+
title: 'Device',
|
|
50
|
+
renderer: (session) => <span className="text-sm">{session.deviceLabel}</span>,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'location',
|
|
54
|
+
title: 'Location',
|
|
55
|
+
renderer: (session) => <span className="text-sm">{session.location}</span>,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'ipAddress',
|
|
59
|
+
title: 'IP Address',
|
|
60
|
+
renderer: (session) => <span className="text-sm font-mono">{session.ipAddress || 'N/A'}</span>,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
export default function AdminUserSessionsCard({ userId, initialSessions }: AdminUserSessionsCardProps) {
|
|
65
|
+
const router = useRouter()
|
|
66
|
+
const [sessions, setSessions] = useState(initialSessions)
|
|
67
|
+
const [sessionToRevoke, setSessionToRevoke] = useState<string | null>(null)
|
|
68
|
+
const [isRevoking, setIsRevoking] = useState(false)
|
|
69
|
+
const [confirmRevokeAll, setConfirmRevokeAll] = useState(false)
|
|
70
|
+
|
|
71
|
+
const now = new Date()
|
|
72
|
+
const activeSessions = sessions.filter(s => new Date(s.expiresAt) > now)
|
|
73
|
+
const expiredSessions = sessions.filter(s => new Date(s.expiresAt) <= now)
|
|
74
|
+
|
|
75
|
+
const handleRevokeAll = async () => {
|
|
76
|
+
setIsRevoking(true)
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`/api/admin/users/${userId}/sessions`, { method: 'DELETE' })
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error('Failed to revoke sessions')
|
|
81
|
+
}
|
|
82
|
+
toast.success('All sessions revoked')
|
|
83
|
+
setSessions(sessions.filter(s => new Date(s.expiresAt) <= now))
|
|
84
|
+
setConfirmRevokeAll(false)
|
|
85
|
+
router.refresh()
|
|
86
|
+
} catch (error) {
|
|
87
|
+
toast.error('Failed to revoke sessions')
|
|
88
|
+
console.error(error)
|
|
89
|
+
} finally {
|
|
90
|
+
setIsRevoking(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleRevokeSession = async (sessionId: string) => {
|
|
95
|
+
setIsRevoking(true)
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(
|
|
98
|
+
`/api/admin/users/${userId}/sessions?sessionId=${sessionId}`,
|
|
99
|
+
{ method: 'DELETE' }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error('Failed to revoke session')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toast.success('Session revoked successfully')
|
|
107
|
+
setSessions(sessions.filter(s => s.id !== sessionId))
|
|
108
|
+
setSessionToRevoke(null)
|
|
109
|
+
router.refresh()
|
|
110
|
+
} catch (error) {
|
|
111
|
+
toast.error('Failed to revoke session')
|
|
112
|
+
console.error(error)
|
|
113
|
+
} finally {
|
|
114
|
+
setIsRevoking(false)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<div className="flex items-start justify-between gap-4">
|
|
123
|
+
<div>
|
|
124
|
+
<CardTitle>Sessions</CardTitle>
|
|
125
|
+
<CardDescription>
|
|
126
|
+
{activeSessions.length} active session(s)
|
|
127
|
+
</CardDescription>
|
|
128
|
+
</div>
|
|
129
|
+
{activeSessions.length > 0 && (
|
|
130
|
+
<Button
|
|
131
|
+
variant="destructive"
|
|
132
|
+
size="sm"
|
|
133
|
+
onClick={() => setConfirmRevokeAll(true)}
|
|
134
|
+
>
|
|
135
|
+
Revoke all sessions
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</CardHeader>
|
|
140
|
+
<CardContent>
|
|
141
|
+
{activeSessions.length === 0 ? (
|
|
142
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 text-center py-8">
|
|
143
|
+
No active sessions found
|
|
144
|
+
</p>
|
|
145
|
+
) : (
|
|
146
|
+
<DataTable
|
|
147
|
+
columns={[
|
|
148
|
+
...baseColumns,
|
|
149
|
+
{
|
|
150
|
+
name: 'actions',
|
|
151
|
+
title: 'Actions',
|
|
152
|
+
renderer: (session) => (
|
|
153
|
+
<Button
|
|
154
|
+
variant="destructive"
|
|
155
|
+
size="sm"
|
|
156
|
+
onClick={() => setSessionToRevoke(session.id)}
|
|
157
|
+
>
|
|
158
|
+
Revoke
|
|
159
|
+
</Button>
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
]}
|
|
163
|
+
data={activeSessions}
|
|
164
|
+
getRowId={(session) => session.id}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{expiredSessions.length > 0 && (
|
|
169
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4">
|
|
170
|
+
{expiredSessions.length} expired session(s) not shown
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
</CardContent>
|
|
174
|
+
</Card>
|
|
175
|
+
|
|
176
|
+
<AlertDialog
|
|
177
|
+
open={!!sessionToRevoke}
|
|
178
|
+
onOpenChange={() => setSessionToRevoke(null)}
|
|
179
|
+
>
|
|
180
|
+
<AlertDialogContent>
|
|
181
|
+
<AlertDialogHeader>
|
|
182
|
+
<AlertDialogTitle>Revoke Session?</AlertDialogTitle>
|
|
183
|
+
<AlertDialogDescription>
|
|
184
|
+
This will log out the device associated with this session.
|
|
185
|
+
This action cannot be undone.
|
|
186
|
+
</AlertDialogDescription>
|
|
187
|
+
</AlertDialogHeader>
|
|
188
|
+
<AlertDialogFooter>
|
|
189
|
+
<AlertDialogCancel disabled={isRevoking}>Cancel</AlertDialogCancel>
|
|
190
|
+
<AlertDialogAction
|
|
191
|
+
onClick={() => sessionToRevoke && handleRevokeSession(sessionToRevoke)}
|
|
192
|
+
disabled={isRevoking}
|
|
193
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
194
|
+
>
|
|
195
|
+
{isRevoking ? "Revoking..." : "Revoke Session"}
|
|
196
|
+
</AlertDialogAction>
|
|
197
|
+
</AlertDialogFooter>
|
|
198
|
+
</AlertDialogContent>
|
|
199
|
+
</AlertDialog>
|
|
200
|
+
|
|
201
|
+
<AlertDialog open={confirmRevokeAll} onOpenChange={setConfirmRevokeAll}>
|
|
202
|
+
<AlertDialogContent>
|
|
203
|
+
<AlertDialogHeader>
|
|
204
|
+
<AlertDialogTitle>Revoke all sessions?</AlertDialogTitle>
|
|
205
|
+
<AlertDialogDescription>
|
|
206
|
+
Logs this account out of every device — use this if the account may be
|
|
207
|
+
compromised. Cached sessions may stay valid for up to 5 minutes. The
|
|
208
|
+
user can sign back in normally afterwards.
|
|
209
|
+
</AlertDialogDescription>
|
|
210
|
+
</AlertDialogHeader>
|
|
211
|
+
<AlertDialogFooter>
|
|
212
|
+
<AlertDialogCancel disabled={isRevoking}>Cancel</AlertDialogCancel>
|
|
213
|
+
<AlertDialogAction
|
|
214
|
+
onClick={handleRevokeAll}
|
|
215
|
+
disabled={isRevoking}
|
|
216
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
217
|
+
>
|
|
218
|
+
{isRevoking ? "Revoking..." : "Revoke all"}
|
|
219
|
+
</AlertDialogAction>
|
|
220
|
+
</AlertDialogFooter>
|
|
221
|
+
</AlertDialogContent>
|
|
222
|
+
</AlertDialog>
|
|
223
|
+
</>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../ui/card"
|
|
2
|
+
import { Badge } from "../../ui/badge"
|
|
3
|
+
import { Separator } from "../../ui/separator"
|
|
4
|
+
import { formatDateTime } from "../../../lib/formatters"
|
|
5
|
+
|
|
6
|
+
type User = {
|
|
7
|
+
banned: boolean
|
|
8
|
+
bannedAt: Date | null
|
|
9
|
+
bannedReason: string | null
|
|
10
|
+
lastLoginAt: Date | null
|
|
11
|
+
lastActiveAt: Date | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AdminUserStatusCard({ user }: { user: User }) {
|
|
15
|
+
return (
|
|
16
|
+
<Card>
|
|
17
|
+
<CardHeader>
|
|
18
|
+
<CardTitle>Account Status</CardTitle>
|
|
19
|
+
</CardHeader>
|
|
20
|
+
<CardContent className="space-y-4">
|
|
21
|
+
<div>
|
|
22
|
+
<p className="text-sm font-medium text-gray-600">Status</p>
|
|
23
|
+
<div className="mt-1">
|
|
24
|
+
{user.banned ? (
|
|
25
|
+
<Badge variant="destructive">Banned</Badge>
|
|
26
|
+
) : (
|
|
27
|
+
<Badge variant="outline">Active</Badge>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
{user.banned && user.bannedReason && (
|
|
31
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
32
|
+
{user.bannedReason}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<Separator />
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<p className="text-sm font-medium text-gray-600">Last Login</p>
|
|
41
|
+
<p className="text-base">
|
|
42
|
+
{user.lastLoginAt ? formatDateTime(user.lastLoginAt) : 'Never'}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Separator />
|
|
47
|
+
|
|
48
|
+
<div>
|
|
49
|
+
<p className="text-sm font-medium text-gray-600">Last Active</p>
|
|
50
|
+
<p className="text-base">
|
|
51
|
+
{user.lastActiveAt ? formatDateTime(user.lastActiveAt) : 'Never'}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</CardContent>
|
|
55
|
+
</Card>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
|
|
2
|
+
import { Skeleton } from "../../ui/skeleton"
|
|
3
|
+
|
|
4
|
+
export default function AdminUsersSkeleton() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Card>
|
|
8
|
+
<CardHeader>
|
|
9
|
+
<CardTitle>Search Users</CardTitle>
|
|
10
|
+
<CardDescription>Search by name or email</CardDescription>
|
|
11
|
+
</CardHeader>
|
|
12
|
+
<CardContent>
|
|
13
|
+
<Skeleton className="h-9 max-w-sm w-full" />
|
|
14
|
+
</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
|
|
17
|
+
<Card>
|
|
18
|
+
<CardHeader>
|
|
19
|
+
<CardTitle>Users List</CardTitle>
|
|
20
|
+
<CardDescription>Loading...</CardDescription>
|
|
21
|
+
</CardHeader>
|
|
22
|
+
<CardContent className="space-y-4">
|
|
23
|
+
{[...Array(5)].map((_, i) => (
|
|
24
|
+
<Skeleton key={i} className="h-16 w-full" />
|
|
25
|
+
))}
|
|
26
|
+
</CardContent>
|
|
27
|
+
</Card>
|
|
28
|
+
</>
|
|
29
|
+
)
|
|
30
|
+
}
|