Added organization menu
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user