organizations.tsx138 lines · main
1import { useParams } from 'common'
2import { Plus, Search } from 'lucide-react'
3import Head from 'next/head'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useEffect, useState } from 'react'
7import { Button, Skeleton } from 'ui'
8import { Admonition } from 'ui-patterns/admonition'
9import { Input } from 'ui-patterns/DataInputs/Input'
10
11import { NoOrganizationsState } from '@/components/interfaces/Home/ProjectList/EmptyStates'
12import { OrganizationCard } from '@/components/interfaces/Organization/OrganizationCard'
13import { AppLayout } from '@/components/layouts/AppLayout/AppLayout'
14import { DefaultLayout } from '@/components/layouts/DefaultLayout'
15import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
16import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
17import { AlertError } from '@/components/ui/AlertError'
18import { NoSearchResults } from '@/components/ui/NoSearchResults'
19import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
20import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
21import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
22import { withAuth } from '@/hooks/misc/withAuth'
23import { buildStudioPageTitle } from '@/lib/page-title'
24import type { NextPageWithLayout } from '@/types'
25
26const OrganizationsPage: NextPageWithLayout = () => {
27 const router = useRouter()
28 const { appTitle } = useCustomContent(['app:title'])
29 const [search, setSearch] = useState('')
30 const { error: orgNotFoundError, org: orgSlug } = useParams()
31 const orgNotFound = orgNotFoundError === 'org_not_found'
32 const pageTitle = buildStudioPageTitle({
33 section: 'Organizations',
34 brand: appTitle || 'Briven',
35 })
36
37 const {
38 data: organizations = [],
39 error,
40 isPending: isLoading,
41 isError,
42 isSuccess,
43 } = useOrganizationsQuery()
44
45 const organizationCreationEnabled = useIsFeatureEnabled('organizations:create')
46 const filteredOrganizations =
47 search.length === 0
48 ? organizations
49 : organizations?.filter(
50 (x) => x.name.toLowerCase().includes(search) || x.slug.toLowerCase().includes(search)
51 )
52
53 useEffect(() => {
54 // If there are no organizations, force the user to create one
55 // unless the user is on the not found page
56 if (isSuccess && organizations.length <= 0 && !orgNotFound) {
57 router.push('/new')
58 }
59 }, [isSuccess, organizations])
60
61 return (
62 <>
63 <Head>
64 <title>{pageTitle}</title>
65 <meta name="description" content="Briven Studio" />
66 </Head>
67 <ScaffoldContainer>
68 <ScaffoldSection isFullWidth className="flex flex-col gap-y-4">
69 {orgNotFound && (
70 <Admonition
71 type="destructive"
72 title="Organization not found"
73 description={
74 <>
75 The organization <code className="text-code-inline">{orgSlug}</code> does not
76 exist or you do not have permission to access to it. Contact the the owner if you
77 believe this is a mistake.
78 </>
79 }
80 />
81 )}
82
83 {organizations.length > 0 && (
84 <div className="flex items-center justify-between gap-x-2 md:gap-x-3">
85 <Input
86 size="tiny"
87 placeholder="Search for an organization"
88 icon={<Search />}
89 className="w-full flex-1 md:w-64"
90 value={search}
91 onChange={(event) => setSearch(event.target.value)}
92 />
93
94 {organizationCreationEnabled && (
95 <Button asChild icon={<Plus />} type="primary" className="w-min">
96 <Link href={`/new`}>New organization</Link>
97 </Button>
98 )}
99 </div>
100 )}
101
102 {isSuccess && organizations.length === 0 && !isError && <NoOrganizationsState />}
103
104 {search.length > 0 && filteredOrganizations.length === 0 && (
105 <NoSearchResults searchString={search} />
106 )}
107
108 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
109 {isLoading && (
110 <>
111 <Skeleton className="h-[70px] rounded-md" />
112 <Skeleton className="h-[70px] rounded-md" />
113 <Skeleton className="h-[70px] rounded-md" />
114 </>
115 )}
116 {isError && <AlertError error={error} subject="Failed to load organizations" />}
117 {isSuccess &&
118 filteredOrganizations.map((org) => (
119 <OrganizationCard key={org.id} organization={org} />
120 ))}
121 </div>
122 </ScaffoldSection>
123 </ScaffoldContainer>
124 </>
125 )
126}
127
128OrganizationsPage.getLayout = (page) => (
129 <AppLayout>
130 <DefaultLayout hideMobileMenu headerTitle="Organizations">
131 <PageLayout title="Your Organizations" className="max-w-[1200px] lg:px-6 mx-auto">
132 {page}
133 </PageLayout>
134 </DefaultLayout>
135 </AppLayout>
136)
137
138export default withAuth(OrganizationsPage)