| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- import { LOCAL_STORAGE_KEYS } from 'common'
- import { Check, ChevronDown } from 'lucide-react'
- import { useMemo, useState, type ReactNode } from 'react'
- import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
- import {
- CreateOrganizationCard,
- OrganizationCard,
- } from '@/components/interfaces/Organization/OrganizationCard'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- import type { Organization } from '@/types'
- const VISIBLE_ORGANIZATIONS_LIMIT = 3
- const CONNECT_DISCLOSURE_TRIGGER_CLASSNAME =
- 'mx-auto flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2 text-xs text-foreground-lighter transition-colors hover:bg-surface-200 hover:text-foreground'
- export const OrganizationSelector = ({
- organizations,
- selectedSlug,
- disabled = false,
- description,
- createLabel,
- createHrefParams,
- onSelect,
- getOrganizationDescription,
- }: {
- organizations: Organization[]
- selectedSlug?: string | null
- disabled?: boolean
- description?: ReactNode
- createLabel?: string
- createHrefParams?: { [key: string]: string }
- onSelect: (slug: string) => void
- getOrganizationDescription?: (organization: Organization) => ReactNode
- }) => {
- const [showMore, setShowMore] = useState(false)
- const [lastVisitedOrganization] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
- ''
- )
- const { visibleOrganizations, overflowOrganizations } = useMemo(() => {
- const lastVisitedOrg = organizations.find(({ slug }) => slug === lastVisitedOrganization)
- const selectedIndex = organizations.findIndex(({ slug }) => slug === selectedSlug)
- const selectedInOverflow = selectedIndex >= VISIBLE_ORGANIZATIONS_LIMIT
- if (!!lastVisitedOrg) {
- const withoutLastVisited = organizations.filter(
- ({ slug }) => slug !== lastVisitedOrganization
- )
- return {
- visibleOrganizations: [
- lastVisitedOrg,
- ...withoutLastVisited.slice(0, VISIBLE_ORGANIZATIONS_LIMIT - 1),
- ],
- overflowOrganizations: withoutLastVisited.slice(VISIBLE_ORGANIZATIONS_LIMIT - 1),
- }
- }
- if (!selectedInOverflow || !selectedSlug) {
- return {
- visibleOrganizations: organizations.slice(0, VISIBLE_ORGANIZATIONS_LIMIT),
- overflowOrganizations: organizations.slice(VISIBLE_ORGANIZATIONS_LIMIT),
- }
- }
- const selected = organizations[selectedIndex]
- const withoutSelected = organizations.filter(({ slug }) => slug !== selectedSlug)
- return {
- visibleOrganizations: [
- ...withoutSelected.slice(0, VISIBLE_ORGANIZATIONS_LIMIT - 1),
- selected,
- ],
- overflowOrganizations: withoutSelected.slice(VISIBLE_ORGANIZATIONS_LIMIT - 1),
- }
- }, [lastVisitedOrganization, organizations, selectedSlug])
- const hasOverflow = overflowOrganizations.length > 0
- return (
- <section className="space-y-2" aria-label="Organizations">
- <div className="space-y-1">
- <p className="text-xs font-medium uppercase tracking-wider text-foreground-light">
- Organization
- </p>
- {description && <p className="text-xs text-foreground-lighter pr-4">{description}</p>}
- </div>
- <div className="space-y-2">
- {visibleOrganizations.map((organization) => (
- <ConnectOrganizationButton
- key={organization.slug}
- organization={organization}
- selected={selectedSlug === organization.slug}
- disabled={disabled}
- onClick={() => onSelect(organization.slug)}
- description={
- getOrganizationDescription?.(organization) ?? getPlanDescription(organization)
- }
- />
- ))}
- {!!createLabel && !!createHrefParams && (
- <CreateOrganizationCard params={createHrefParams} label={createLabel} />
- )}
- {hasOverflow && (
- <Collapsible open={showMore} onOpenChange={setShowMore}>
- <CollapsibleTrigger className={CONNECT_DISCLOSURE_TRIGGER_CLASSNAME}>
- <span>{showMore ? 'Show fewer' : `Show ${overflowOrganizations.length} more`}</span>
- <ChevronDown
- className={cn('size-3.5 transition-transform', showMore && 'rotate-180')}
- />
- </CollapsibleTrigger>
- <CollapsibleContent className="data-closed:animate-collapsible-up data-open:animate-collapsible-down overflow-hidden">
- <div className="space-y-2 pt-1">
- {overflowOrganizations.map((organization) => (
- <ConnectOrganizationButton
- key={organization.slug}
- organization={organization}
- selected={selectedSlug === organization.slug}
- disabled={disabled}
- onClick={() => onSelect(organization.slug)}
- description={
- getOrganizationDescription?.(organization) ?? getPlanDescription(organization)
- }
- />
- ))}
- </div>
- </CollapsibleContent>
- </Collapsible>
- )}
- </div>
- </section>
- )
- }
- const getPlanDescription = (organization: Organization) => `${organization.plan.name} Plan`
- const ConnectOrganizationButton = ({
- organization,
- selected,
- disabled,
- onClick,
- description,
- }: {
- organization: Organization
- selected?: boolean
- disabled?: boolean
- onClick?: () => void
- description?: ReactNode
- }) => (
- <button
- type="button"
- disabled={disabled}
- onClick={onClick}
- aria-pressed={selected}
- className={cn(
- 'group relative block w-full cursor-pointer text-left disabled:cursor-not-allowed disabled:opacity-50',
- disabled && 'pointer-events-none'
- )}
- >
- <OrganizationCard
- isLink={false}
- organization={organization}
- description={description}
- className={cn(
- 'pointer-events-none shadow-none transition-colors',
- !disabled && !selected && 'group-hover:border-default group-hover:bg-surface-200',
- selected &&
- 'border-brand bg-brand-200/20 dark:bg-brand-300 pr-10 group-hover:border-brand group-hover:bg-brand-200/20'
- )}
- />
- {selected && (
- <span className="pointer-events-none absolute right-3 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-full bg-brand-500 dark:bg-brand-200 text-white dark:text-brand">
- <Check className="size-3.5" strokeWidth={2} />
- </span>
- )}
- </button>
- )
|