PageLayout.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { useParams } from 'common'
  2. import Link from 'next/link'
  3. import { useRouter } from 'next/router'
  4. import { ReactNode } from 'react'
  5. import { Badge, Button, cn, NavMenu, NavMenuItem } from 'ui'
  6. import { ScaffoldContainer } from '../Scaffold'
  7. import { PageHeader } from './PageHeader'
  8. export interface NavigationItem {
  9. id?: string
  10. label: string
  11. href?: string
  12. icon?: ReactNode
  13. onClick?: () => void
  14. badge?: string
  15. active?: boolean
  16. }
  17. interface PageLayoutProps {
  18. children?: ReactNode
  19. title?: string | ReactNode
  20. subtitle?: string | ReactNode
  21. icon?: ReactNode
  22. breadcrumbs?: Array<{
  23. label?: string
  24. href?: string
  25. element?: ReactNode
  26. }>
  27. primaryActions?: ReactNode
  28. secondaryActions?: ReactNode
  29. navigationItems?: NavigationItem[]
  30. className?: string
  31. size?: 'default' | 'full' | 'large' | 'small'
  32. isCompact?: boolean
  33. }
  34. /**
  35. * For rendering a general single column based UI (which covers most of the dashboard's use case)
  36. *
  37. * Pages that would use this would for example be: Auth Sign In / Up, Project settings, Advisors, etc
  38. *
  39. * Pages that would deviate from this are those with dedicated UI, for example: Table Editor, SQL Editor, etc
  40. *
  41. * Handles rendering the following UI behaviors:
  42. * - Page padding depending on the size property
  43. * - Top level breadcrumbs (If applicable)
  44. * - Top level page header (If applicable)
  45. * - Top level tab navigations (If applicable)
  46. * @param title - Title rendered in page header
  47. * @param subtitle - Subtitle rendered in page header, below title
  48. * @param icon - Icon rendered in Page header, to the left of title and subtitle
  49. * @param breadcrumbs - Breadcrumbs rendered in page header, above title. Can be string labels with hrefs or custom elements
  50. * @param primaryActions - TBD
  51. * @param secondaryActions - TBD
  52. * @param navigationItems - Tab navigation rendered below the page header
  53. * @param className - Optional additional class names to be applied
  54. * @param size - Controls padding of the page header only, padding for the content to be controlled by PageContainer (Default: 'default')
  55. * @param isCompact - TBD (Default: false)
  56. */
  57. export const PageLayout = ({
  58. children,
  59. title,
  60. subtitle,
  61. icon,
  62. breadcrumbs = [],
  63. primaryActions,
  64. secondaryActions,
  65. navigationItems = [],
  66. className,
  67. size = 'default',
  68. isCompact = false,
  69. }: PageLayoutProps) => {
  70. const { ref } = useParams()
  71. const router = useRouter()
  72. return (
  73. <div className={cn('w-full min-h-full flex flex-col items-stretch', className)}>
  74. <ScaffoldContainer
  75. size={size}
  76. className={cn(
  77. 'w-full mx-auto',
  78. size === 'full' &&
  79. (isCompact ? 'max-w-none px-6! border-b pt-4' : 'max-w-none pt-6 px-10! border-b'),
  80. size !== 'full' && (isCompact ? 'pt-4' : 'pt-12'),
  81. navigationItems.length === 0 && size === 'full' && (isCompact ? 'pb-4' : 'pb-8')
  82. )}
  83. >
  84. {/* Header section */}
  85. {(title || subtitle || primaryActions || secondaryActions || breadcrumbs.length > 0) && (
  86. <PageHeader
  87. title={title}
  88. subtitle={subtitle}
  89. icon={icon}
  90. breadcrumbs={breadcrumbs}
  91. primaryActions={primaryActions}
  92. secondaryActions={secondaryActions}
  93. isCompact={isCompact}
  94. />
  95. )}
  96. {/* Navigation section */}
  97. {navigationItems.length > 0 && (
  98. <NavMenu className={cn(isCompact ? 'mt-2' : 'mt-4', size === 'full' && 'border-none')}>
  99. {navigationItems.map((item) => {
  100. const isActive =
  101. item.active !== undefined ? item.active : router.asPath.split('?')[0] === item.href
  102. return (
  103. <NavMenuItem key={item.label} active={isActive}>
  104. {item.href ? (
  105. <Link
  106. href={
  107. item.href.includes('[ref]') && !!ref
  108. ? item.href.replace('[ref]', ref)
  109. : item.href
  110. }
  111. className={cn(
  112. 'inline-flex items-center gap-2',
  113. isActive && 'text-foreground'
  114. )}
  115. onClick={item.onClick}
  116. >
  117. {item.icon && <span>{item.icon}</span>}
  118. {item.label}
  119. {item.badge && <Badge variant="default">{item.badge}</Badge>}
  120. </Link>
  121. ) : (
  122. <Button
  123. type="link"
  124. onClick={item.onClick}
  125. className={cn(isActive && 'text-foreground font-medium')}
  126. >
  127. {item.icon && <span className="mr-2">{item.icon}</span>}
  128. {item.label}
  129. {item.badge && <Badge variant="default">{item.badge}</Badge>}
  130. </Button>
  131. )}
  132. </NavMenuItem>
  133. )
  134. })}
  135. </NavMenu>
  136. )}
  137. </ScaffoldContainer>
  138. {/* Content section */}
  139. {children}
  140. </div>
  141. )
  142. }