Vectrum
Sign In
85 lines · 3.5 KB · TypeScript
import type { Metadata } from "next";
import Link from "next/link";
import { Label as MuLabel } from "@unlocalhosted/metalui";
import { leaveOrgAction } from "@/app/actions/orgs";
import { ActionButton } from "@/components/action-button";
import { GearIcon, OrganizationIcon, PlusIcon } from "@/components/icons";
import { SettingsSection } from "@/components/metal";
import { OrgInvitations } from "@/components/org-invitations";
import { ListRow } from "@/components/settings-plate";
import { Avatar, avatarUrl, Blankslate, ButtonLink, Label, PageTitle } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { timeAgo } from "@/lib/format";
import { listInvitationsFor, listOrgsOf, ROLE_INFO } from "@/lib/orgs";

export const metadata: Metadata = { title: "Organizations" };

export default async function OrganizationsSettingsPage() {
  const viewer = await requireViewer("/settings/organizations");
  const [memberships, invitations] = await Promise.all([listOrgsOf(viewer.id), listInvitationsFor(viewer.id)]);
  return (
    <div>
      <PageTitle
        title="Organizations"
        description="Organizations own repositories for a team. Their owners invite people and choose what each of them may do."
        actions={
          <ButtonLink href="/organizations/new" variant="primary" icon={<PlusIcon />}>
            New Organization
          </ButtonLink>
        }
      />
      <div className="flex flex-col gap-7">
        {invitations.length > 0 && (
          <section className="flex flex-col gap-2">
            <h2 className="px-1.5 type-label engraved">Invitations · {invitations.length}</h2>
            <OrgInvitations invitations={invitations} />
          </section>
        )}
        <SettingsSection title={`Your organizations · ${memberships.length}`}>
          {memberships.map(({ org, role, joinedAt }) => (
            <ListRow
              key={org.id}
              lead={<Avatar name={org.name || org.username} src={avatarUrl(org)} size={32} />}
              title={
                <>
                  <Link href={`/${org.username}`} className="truncate hover:underline">
                    {org.username}
                  </Link>
                  <Label tone={role === "owner" ? "accent" : "default"}>{ROLE_INFO[role].title}</Label>
                </>
              }
              meta={
                <MuLabel variant="engraved">
                  {org.name && (
                    <>
                      <b>{org.name}</b> ·{" "}
                    </>
                  )}
                  Joined <b>{timeAgo(joinedAt)}</b>
                </MuLabel>
              }
            >
              {role === "owner" && (
                <ButtonLink href={`/organizations/${org.username}/settings`} icon={<GearIcon />}>
                  Settings
                </ButtonLink>
              )}
              <ActionButton
                action={leaveOrgAction.bind(null, org.username)}
                confirm={{
                  title: `Leave ${org.username}?`,
                  description: "You lose access to its private repositories. To come back, an owner has to invite you again.",
                  label: "Leave",
                  destructive: true,
                }}
              >
                Leave
              </ActionButton>
            </ListRow>
          ))}
          {memberships.length === 0 && <Blankslate icon={<OrganizationIcon />} title="You’re not a member of any organization" />}
        </SettingsSection>
      </div>
    </div>
  );
}