panel-wrapper.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { type ReactNode } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { cn } from '@/lib/utils'
  4. import { Skeleton } from '@/components/ui/skeleton'
  5. interface PanelWrapperProps {
  6. title: ReactNode
  7. description?: ReactNode
  8. loading?: boolean
  9. empty?: boolean
  10. emptyMessage?: string
  11. height?: string
  12. className?: string
  13. contentClassName?: string
  14. headerActions?: ReactNode
  15. children?: ReactNode
  16. }
  17. function PanelHeader(props: {
  18. title: ReactNode
  19. description?: ReactNode
  20. actions?: ReactNode
  21. }) {
  22. const heading = (
  23. <div className='flex flex-col gap-1'>
  24. <div className='text-sm font-semibold'>{props.title}</div>
  25. {props.description != null && (
  26. <div className='text-muted-foreground text-xs'>{props.description}</div>
  27. )}
  28. </div>
  29. )
  30. return (
  31. <div className='border-b px-4 py-3 sm:px-5'>
  32. {props.actions != null ? (
  33. <div className='flex items-start justify-between gap-2'>
  34. {heading}
  35. {props.actions}
  36. </div>
  37. ) : (
  38. heading
  39. )}
  40. </div>
  41. )
  42. }
  43. export function PanelWrapper(props: PanelWrapperProps) {
  44. const { t } = useTranslation()
  45. const resolvedEmptyMessage = props.emptyMessage ?? t('No data available')
  46. const height = props.height ?? 'h-64'
  47. const frameClassName = cn(
  48. 'overflow-hidden rounded-2xl border bg-card shadow-xs',
  49. props.className
  50. )
  51. if (props.loading) {
  52. return (
  53. <div className={frameClassName}>
  54. <PanelHeader title={props.title} description={props.description} />
  55. <div className={cn('p-4 sm:p-5', props.contentClassName)}>
  56. <Skeleton className={`w-full ${height}`} />
  57. </div>
  58. </div>
  59. )
  60. }
  61. if (props.empty) {
  62. return (
  63. <div className={frameClassName}>
  64. <PanelHeader title={props.title} description={props.description} />
  65. <div
  66. className={cn(
  67. 'text-muted-foreground flex items-center justify-center px-4 text-sm',
  68. height,
  69. props.contentClassName
  70. )}
  71. >
  72. {resolvedEmptyMessage}
  73. </div>
  74. </div>
  75. )
  76. }
  77. return (
  78. <div className={frameClassName}>
  79. <PanelHeader
  80. title={props.title}
  81. description={props.description}
  82. actions={props.headerActions}
  83. />
  84. <div className={cn('p-4 sm:p-5', props.contentClassName)}>
  85. {props.children}
  86. </div>
  87. </div>
  88. )
  89. }