profile-header.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { Activity, BarChart3, WalletCards } from 'lucide-react'
  2. import { useTranslation } from 'react-i18next'
  3. import { formatCompactNumber, formatQuota } from '@/lib/format'
  4. import { getRoleLabel } from '@/lib/roles'
  5. import { Avatar, AvatarFallback } from '@/components/ui/avatar'
  6. import { Skeleton } from '@/components/ui/skeleton'
  7. import { StatusBadge } from '@/components/status-badge'
  8. import { getUserInitials, getDisplayName } from '../lib'
  9. import type { UserProfile } from '../types'
  10. // ============================================================================
  11. // Profile Header Component
  12. // ============================================================================
  13. interface ProfileHeaderProps {
  14. profile: UserProfile | null
  15. loading: boolean
  16. }
  17. export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
  18. const { t } = useTranslation()
  19. if (loading) {
  20. return (
  21. <div className='bg-card overflow-hidden rounded-lg border'>
  22. <div className='p-4 sm:p-5'>
  23. <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
  24. <Skeleton className='h-16 w-16 rounded-2xl' />
  25. <div className='space-y-3'>
  26. <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
  27. <Skeleton className='h-8 w-48' />
  28. <Skeleton className='h-5 w-16' />
  29. </div>
  30. <div className='flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'>
  31. <Skeleton className='h-4 w-24' />
  32. <Skeleton className='h-4 w-40' />
  33. <Skeleton className='h-4 w-20' />
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <div className='border-t'>
  39. <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
  40. {Array.from({ length: 3 }).map((_, i) => (
  41. <div key={i} className='px-4 py-3.5 sm:px-5 sm:py-4'>
  42. <Skeleton className='h-3.5 w-20' />
  43. <Skeleton className='mt-2 h-7 w-28' />
  44. <Skeleton className='mt-1.5 h-3.5 w-24' />
  45. </div>
  46. ))}
  47. </div>
  48. </div>
  49. </div>
  50. )
  51. }
  52. if (!profile) return null
  53. const displayName = getDisplayName(profile)
  54. const initials = getUserInitials(profile)
  55. const roleLabel = getRoleLabel(profile.role)
  56. const stats = [
  57. {
  58. label: t('Current Balance'),
  59. value: formatQuota(profile.quota),
  60. description: t('Remaining quota'),
  61. icon: WalletCards,
  62. },
  63. {
  64. label: t('Total Usage'),
  65. value: formatQuota(profile.used_quota),
  66. description: t('Total consumed quota'),
  67. icon: BarChart3,
  68. },
  69. {
  70. label: t('API Requests'),
  71. value: formatCompactNumber(profile.request_count),
  72. description: t('Total requests made'),
  73. icon: Activity,
  74. },
  75. ]
  76. return (
  77. <div className='bg-card overflow-hidden rounded-lg border'>
  78. <div className='p-3 sm:p-5'>
  79. <div className='flex items-center gap-3 text-left sm:gap-4'>
  80. <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'>
  81. <AvatarFallback className='bg-primary/10 text-primary rounded-xl sm:rounded-2xl'>
  82. {initials}
  83. </AvatarFallback>
  84. </Avatar>
  85. <div className='min-w-0 flex-1 space-y-1.5 sm:space-y-3'>
  86. <div className='flex min-w-0 items-center gap-2'>
  87. <h1 className='truncate text-xl font-semibold tracking-tight sm:text-2xl'>
  88. {displayName}
  89. </h1>
  90. <StatusBadge
  91. label={roleLabel}
  92. variant='neutral'
  93. copyable={false}
  94. />
  95. </div>
  96. <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'>
  97. <span className='truncate'>@{profile.username}</span>
  98. {profile.email && (
  99. <>
  100. <span>•</span>
  101. <span className='truncate'>{profile.email}</span>
  102. </>
  103. )}
  104. {profile.group && (
  105. <>
  106. <span>•</span>
  107. <span className='truncate'>{profile.group}</span>
  108. </>
  109. )}
  110. </div>
  111. </div>
  112. </div>
  113. </div>
  114. <div className='border-t'>
  115. <div className='divide-border/60 grid grid-cols-3 divide-x'>
  116. {stats.map((item) => (
  117. <div key={item.label} className='min-w-0 px-3 py-3 sm:px-5 sm:py-4'>
  118. <div className='flex items-center gap-2'>
  119. <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
  120. <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
  121. {item.label}
  122. </div>
  123. </div>
  124. <div className='text-foreground mt-1.5 truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
  125. {item.value}
  126. </div>
  127. <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
  128. {item.description}
  129. </div>
  130. </div>
  131. ))}
  132. </div>
  133. </div>
  134. </div>
  135. )
  136. }