chore: update user detail viewer
This commit is contained in:
parent
4e6496adc0
commit
d889cdf2b7
@ -442,11 +442,13 @@
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"email": {
|
||||
"verified": "Verified",
|
||||
"unverified": "Unverified"
|
||||
"verified": "Email Verified",
|
||||
"unverified": "Email Unverified"
|
||||
},
|
||||
"banned": "Banned",
|
||||
"active": "Active",
|
||||
"joined": "Joined",
|
||||
"updated": "Updated",
|
||||
"ban": {
|
||||
"reason": "Ban Reason",
|
||||
"reasonPlaceholder": "Enter the reason for banning this user",
|
||||
|
@ -443,11 +443,13 @@
|
||||
"users": {
|
||||
"title": "用户管理",
|
||||
"email": {
|
||||
"verified": "已验证",
|
||||
"unverified": "未验证"
|
||||
"verified": "邮箱已验证",
|
||||
"unverified": "邮箱未验证"
|
||||
},
|
||||
"banned": "已封禁",
|
||||
"banned": "账号被封禁",
|
||||
"active": "账号正常",
|
||||
"joined": "加入时间",
|
||||
"updated": "更新时间",
|
||||
"ban": {
|
||||
"reason": "封禁原因",
|
||||
"reasonPlaceholder": "请输入封禁该用户的原因",
|
||||
|
@ -1,9 +1,3 @@
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UserAvatar } from '@/components/layout/user-avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -22,8 +16,18 @@ import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { Loader2Icon, MailCheckIcon, MailQuestionIcon } from 'lucide-react';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import {
|
||||
Loader2Icon,
|
||||
MailCheckIcon,
|
||||
MailQuestionIcon,
|
||||
UserRoundCheckIcon,
|
||||
UserRoundXIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { SafeActionResult } from 'next-safe-action';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@ -33,6 +37,7 @@ export interface User {
|
||||
image: string | null;
|
||||
role: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerId: string | null;
|
||||
banned: boolean | null;
|
||||
banReason: string | null;
|
||||
@ -166,7 +171,15 @@ export function UserDetailViewer({
|
||||
className="size-12 border"
|
||||
/>
|
||||
<div>
|
||||
<DrawerTitle>{user.name}</DrawerTitle>
|
||||
<DrawerTitle className="flex items-center gap-2">
|
||||
{user.name}
|
||||
<Badge
|
||||
variant={user.role === 'admin' ? 'default' : 'outline'}
|
||||
className="px-1.5"
|
||||
>
|
||||
{user.role || 'user'}
|
||||
</Badge>
|
||||
</DrawerTitle>
|
||||
<DrawerDescription>{user.email}</DrawerDescription>
|
||||
</div>
|
||||
</div>
|
||||
@ -174,7 +187,11 @@ export function UserDetailViewer({
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{/* email verified */}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-muted-foreground px-1.5 hover:bg-accent"
|
||||
>
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
@ -184,24 +201,37 @@ export function UserDetailViewer({
|
||||
? t('email.verified')
|
||||
: t('email.unverified')}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={user.role === 'admin' ? 'default' : 'outline'}
|
||||
className="px-1.5"
|
||||
>
|
||||
{user.role || 'user'}
|
||||
</Badge>
|
||||
{user.banned && (
|
||||
<Badge variant="destructive" className="px-1.5">
|
||||
{t('banned')}
|
||||
|
||||
{/* user banned */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-muted-foreground px-1.5 hover:bg-accent"
|
||||
>
|
||||
{user.banned ? (
|
||||
<UserRoundXIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
) : (
|
||||
<UserRoundCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
)}
|
||||
{user.banned ? t('banned') : t('active')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* information */}
|
||||
<div className="text-muted-foreground">
|
||||
{t('joined')}: {formatDate(user.createdAt)}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('joined')}: {format(user.createdAt, 'PPP')}
|
||||
{t('updated')}: {formatDate(user.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
{/* error */}
|
||||
{error && <div className="text-sm text-destructive">{error}</div>}
|
||||
|
||||
{/* ban or unban user */}
|
||||
{user.banned ? (
|
||||
<div className="grid gap-4">
|
||||
<div className="text-muted-foreground">
|
||||
@ -209,13 +239,14 @@ export function UserDetailViewer({
|
||||
</div>
|
||||
{user.banExpires && (
|
||||
<div className="text-muted-foreground">
|
||||
{t('ban.expires')}: {format(user.banExpires, 'PPP')}
|
||||
{t('ban.expires')}: {formatDate(user.banExpires)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleUnban}
|
||||
disabled={isLoading}
|
||||
className="mt-4"
|
||||
>
|
||||
{isLoading && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
@ -254,6 +285,7 @@ export function UserDetailViewer({
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
disabled={isLoading || !banReason}
|
||||
className="mt-4"
|
||||
>
|
||||
{isLoading && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
@ -95,6 +96,7 @@ export interface User {
|
||||
image: string | null;
|
||||
role: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerId: string | null;
|
||||
banned: boolean | null;
|
||||
banReason: string | null;
|
||||
@ -219,7 +221,7 @@ const columns: ColumnDef<User>[] = [
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.customerId ? (
|
||||
<a
|
||||
href={`https://dashboard.stripe.com/customers/${user.customerId}`}
|
||||
href={getStripeDashboardCustomerUrl(user.customerId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline hover:underline-offset-4"
|
||||
|
@ -78,3 +78,15 @@ export function getUrlWithLocaleInCallbackUrl(
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Stripe dashboard customer URL
|
||||
* @param customerId - The Stripe customer ID
|
||||
* @returns The Stripe dashboard customer URL
|
||||
*/
|
||||
export function getStripeDashboardCustomerUrl(customerId: string): string {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `https://dashboard.stripe.com/test/customers/${customerId}`;
|
||||
}
|
||||
return `https://dashboard.stripe.com/customers/${customerId}`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user