Added organization menu

This commit is contained in:
Matt Aitken
2022-12-09 15:50:38 +00:00
parent 8604db8bdc
commit 42e181cd7f
8 changed files with 141 additions and 11 deletions
+3
View File
@@ -2,6 +2,7 @@ import { DocumentTextIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { useOptionalUser } from "~/hooks/useUser";
import { Logo } from "./Logo";
import { OrganizationMenu } from "./navigation/OrganizationMenu";
import { UserProfileMenu } from "./UserProfileMenu";
type HeaderProps = {
@@ -17,6 +18,8 @@ export function Header({ children }: HeaderProps) {
<Logo className="h-6" />
</Link>
<OrganizationMenu />
<div className="flex flex-1 justify-center">{children}</div>
<div className="flex items-center gap-2">
@@ -0,0 +1,114 @@
import { Organization } from ".prisma/client";
import { Popover, Transition } from "@headlessui/react";
import {
BookmarkIcon,
BriefcaseIcon,
CheckIcon,
ChevronDownIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import classNames from "classnames";
import React, { Fragment } from "react";
import {
useCurrentOrganizationSlug,
useOrganizations,
} from "~/hooks/useOrganizations";
const actionClassNames = "text-green-500";
export function OrganizationMenu() {
const organizations = useOrganizations();
const currentOrganizationSlug = useCurrentOrganizationSlug();
const currentOrganization = organizations?.find(
(org) => org.slug === currentOrganizationSlug
);
if (organizations === undefined) {
return null;
}
return (
<div className="w-full max-w-max px-4">
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`
${open ? "" : "text-opacity-90"}
group inline-flex min-w-[200px] justify-between items-center rounded text-slate-700 bg-white pl-2.5 pr-2 py-1 text-sm border border-slate-200 shadow-sm hover:bg-slate-50 transition focus:outline-none`}
>
<span className="transition">
{currentOrganization ? (
<span>{currentOrganization.title}</span>
) : (
<span className={actionClassNames}>
Create new Organization
</span>
)}
</span>
<ChevronDownIcon
className={`${open ? "rotate-180" : "text-opacity-70"}
ml-1 h-5 w-5 transition duration-150 ease-in-out group-hover:text-opacity-80`}
aria-hidden="true"
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute left-1/2 z-10 mt-3 w-screen min-w-max max-w-xs -translate-x-1/2 transform px-4 sm:px-0">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid py-1 bg-white grid-cols-1">
{organizations.map((organization) => {
return (
<Popover.Button
key={organization.id}
as={Link}
to={`/orgs/${organization.slug}`}
className={classNames(
"flex items-center justify-between gap-1.5 mx-1 px-3 py-2 text-slate-600 rounded hover:bg-slate-100 transition",
organization.slug === currentOrganizationSlug &&
"!bg-slate-200"
)}
>
<div className="flex items-center gap-2">
<BookmarkIcon
className="h-5 w-5 z-100"
aria-hidden="true"
/>
<span className="block truncate">
{organization.title}
</span>
</div>
{organization.slug === currentOrganizationSlug && (
<CheckIcon className="h-5 w-5 text-blue-500" />
)}
</Popover.Button>
);
})}
<Popover.Button as={Link} to={`/orgs/new`}>
<div className="flex items-center gap-2 mx-1 mt-1 pl-1 py-2 rounded bg-white hover:bg-slate-100 transition">
<PlusIcon
className="h-5 w-5 text-green-500"
aria-hidden="true"
/>
<span className="text-slate-600">New Organization</span>
</div>
</Popover.Button>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
);
}
+8 -3
View File
@@ -15,10 +15,15 @@ function isOrganizations(orgs: any): orgs is Organization[] {
}
export function useOrganizations(): Organization[] | undefined {
const data = useMatchesData("routes/__app");
const routeMatch = useMatchesData("routes/__app");
if (!data || !isOrganizations(data.organizations)) {
if (!routeMatch || !isOrganizations(routeMatch.data.organizations)) {
return undefined;
}
return data.organizations;
return routeMatch.data.organizations;
}
export function useCurrentOrganizationSlug(): string | undefined {
const routeMatch = useMatchesData("routes/__app/orgs/$organizationSlug");
return routeMatch?.params?.organizationSlug;
}
+3 -3
View File
@@ -6,11 +6,11 @@ function isUser(user: any): user is User {
}
export function useOptionalUser(): User | undefined {
const data = useMatchesData("root");
if (!data || !isUser(data.user)) {
const routeMatch = useMatchesData("root");
if (!routeMatch || !isUser(routeMatch.data.user)) {
return undefined;
}
return data.user;
return routeMatch.data.user;
}
export function useUser(): User {
+1 -1
View File
@@ -9,7 +9,7 @@ export default function AppLayout() {
return (
<>
<Header>Home</Header>
<Header />
<AppBody>
<div className="flex items-center justify-center p-12 ">
<ul className="grid grid-cols-3 max-w-5xl gap-2">
@@ -25,7 +25,6 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export default function Organization() {
return (
<>
Organizations
<Outlet />
</>
);
@@ -1,3 +1,11 @@
import { Header } from "~/components/Header";
import { AppBody } from "~/components/layout/AppLayout";
export default function OrganizationBlankState() {
return <>You need to create a workflow</>;
return (
<>
<Header />
<AppBody>You need to create a workflow</AppBody>
</>
);
}
+3 -2
View File
@@ -1,3 +1,4 @@
import type { RouteMatch } from "@remix-run/react";
import { useMatches } from "@remix-run/react";
import { useMemo } from "react";
@@ -34,7 +35,7 @@ export function safeRedirect(
export function useMatchesData(
id: string,
debug: boolean = false
): Record<string, unknown> | undefined {
): RouteMatch | undefined {
const matchingRoutes = useMatches();
if (debug) {
console.log("matchingRoutes", matchingRoutes);
@@ -44,7 +45,7 @@ export function useMatchesData(
() => matchingRoutes.find((route) => route.id === id),
[matchingRoutes, id]
);
return route?.data;
return route;
}
export function validateEmail(email: unknown): email is string {