chore: update user detail viewer

This commit is contained in:
javayhu 2025-05-11 09:55:21 +08:00
parent 4e6496adc0
commit d889cdf2b7
5 changed files with 77 additions and 27 deletions

View File

@ -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",

View File

@ -443,11 +443,13 @@
"users": {
"title": "用户管理",
"email": {
"verified": "已验证",
"unverified": "未验证"
"verified": "邮箱已验证",
"unverified": "邮箱未验证"
},
"banned": "已封禁",
"banned": "账号被封禁",
"active": "账号正常",
"joined": "加入时间",
"updated": "更新时间",
"ban": {
"reason": "封禁原因",
"reasonPlaceholder": "请输入封禁该用户的原因",

View File

@ -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" />

View File

@ -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"

View File

@ -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}`;
}