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