♻️ refactor(web): consolidate design-system primitives and responsive data views
This commit is contained in:
+29
-60
@@ -16,10 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { Activity, BarChart3, WalletCards } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -30,10 +29,6 @@ import { getRoleLabel } from '@/lib/roles'
|
||||
import { getDisplayName } from '../lib'
|
||||
import type { UserProfile } from '../types'
|
||||
|
||||
// ============================================================================
|
||||
// Profile Header Component
|
||||
// ============================================================================
|
||||
|
||||
interface ProfileHeaderProps {
|
||||
profile: UserProfile | null
|
||||
loading: boolean
|
||||
@@ -46,28 +41,20 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
return (
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardContent className='p-4 sm:p-5'>
|
||||
<div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
|
||||
<Skeleton className='h-16 w-16 rounded-2xl' />
|
||||
<div className='space-y-3'>
|
||||
<div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
|
||||
<Skeleton className='h-8 w-48' />
|
||||
<Skeleton className='h-5 w-16' />
|
||||
</div>
|
||||
<div className='flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'>
|
||||
<Skeleton className='h-4 w-24' />
|
||||
<Skeleton className='h-4 w-40' />
|
||||
<Skeleton className='h-4 w-20' />
|
||||
</div>
|
||||
<div className='flex items-center gap-3 sm:gap-4'>
|
||||
<Skeleton className='size-12 rounded-xl sm:size-14' />
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-6 w-48' />
|
||||
<Skeleton className='h-4 w-64' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='border-t'>
|
||||
<div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className='px-4 py-3.5 sm:px-5 sm:py-4'>
|
||||
<Skeleton className='h-3.5 w-20' />
|
||||
<div className='divide-border/60 grid grid-cols-3 divide-x'>
|
||||
{['balance', 'usage', 'requests'].map((key) => (
|
||||
<div key={key} className='px-4 py-3 sm:px-5 sm:py-4'>
|
||||
<Skeleton className='h-4 w-24' />
|
||||
<Skeleton className='mt-2 h-7 w-28' />
|
||||
<Skeleton className='mt-1.5 h-3.5 w-24' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -87,64 +74,53 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
{
|
||||
label: t('Current Balance'),
|
||||
value: formatQuota(profile.quota),
|
||||
description: t('Remaining quota'),
|
||||
icon: WalletCards,
|
||||
},
|
||||
{
|
||||
label: t('Total Usage'),
|
||||
value: formatQuota(profile.used_quota),
|
||||
description: t('Total consumed quota'),
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
label: t('API Requests'),
|
||||
value: formatCompactNumber(profile.request_count),
|
||||
description: t('Total requests made'),
|
||||
icon: Activity,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardContent className='p-3 sm:p-5'>
|
||||
<div className='flex items-center gap-3 text-left sm:gap-4'>
|
||||
<Avatar className='ring-background h-12 w-12 rounded-xl text-sm ring-2 sm:h-16 sm:w-16 sm:rounded-2xl sm:text-lg sm:ring-4'>
|
||||
<CardContent className='p-4 sm:p-5'>
|
||||
<div className='flex items-center gap-3 sm:gap-4'>
|
||||
<Avatar className='size-12 rounded-xl text-base sm:size-14 sm:text-lg'>
|
||||
<AvatarFallback
|
||||
className='rounded-xl font-semibold text-white sm:rounded-2xl'
|
||||
className='rounded-xl font-semibold text-white'
|
||||
style={avatarFallbackStyle}
|
||||
>
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className='min-w-0 flex-1 space-y-1.5 sm:space-y-3'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
<h1 className='truncate text-xl font-semibold tracking-tight sm:text-2xl'>
|
||||
<div className='min-w-0 flex-1 space-y-1'>
|
||||
<div className='flex min-w-0 flex-wrap items-center gap-x-2.5 gap-y-1'>
|
||||
<h1 className='truncate text-xl font-semibold tracking-tight'>
|
||||
{displayName}
|
||||
</h1>
|
||||
<StatusBadge
|
||||
label={roleLabel}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
<StatusBadge
|
||||
label={`${t('User ID')} ${profile.id}`}
|
||||
variant='info'
|
||||
copyText={String(profile.id)}
|
||||
/>
|
||||
<StatusBadge className='shrink-0'>{roleLabel}</StatusBadge>
|
||||
</div>
|
||||
|
||||
<div className='text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs sm:gap-x-4 sm:text-sm'>
|
||||
<div className='text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm'>
|
||||
<span className='truncate'>@{profile.username}</span>
|
||||
<span aria-hidden='true'>·</span>
|
||||
<CopyableStatusBadge value={String(profile.id)}>
|
||||
ID {profile.id}
|
||||
</CopyableStatusBadge>
|
||||
{profile.email && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span aria-hidden='true'>·</span>
|
||||
<span className='truncate'>{profile.email}</span>
|
||||
</>
|
||||
)}
|
||||
{profile.group && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span aria-hidden='true'>·</span>
|
||||
<span className='truncate'>{profile.group}</span>
|
||||
</>
|
||||
)}
|
||||
@@ -155,20 +131,13 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
<div className='border-t'>
|
||||
<div className='divide-border/60 grid grid-cols-3 divide-x'>
|
||||
{stats.map((item) => (
|
||||
<div key={item.label} className='min-w-0 px-3 py-3 sm:px-5 sm:py-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
|
||||
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
||||
{item.label}
|
||||
</div>
|
||||
<div key={item.label} className='min-w-0 px-4 py-3 sm:px-5 sm:py-4'>
|
||||
<div className='text-muted-foreground truncate text-sm'>
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
<div className='text-foreground mt-1.5 truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
||||
<div className='text-foreground mt-1 truncate text-lg font-semibold tracking-tight tabular-nums sm:text-2xl'>
|
||||
{item.value}
|
||||
</div>
|
||||
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user