organizations.tsx 5.0 KB

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