diff --git a/app/src/components/nav/NavSidebarOption.tsx b/app/src/components/nav/NavSidebarOption.tsx
index 9c3afea..052e4b5 100644
--- a/app/src/components/nav/NavSidebarOption.tsx
+++ b/app/src/components/nav/NavSidebarOption.tsx
@@ -3,8 +3,9 @@ import { useRouter } from "next/router";
const NavSidebarOption = ({
activeHrefPattern,
+ disableHoverEffect,
...props
-}: { activeHrefPattern?: string } & BoxProps) => {
+}: { activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps) => {
const router = useRouter();
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
return (
@@ -12,7 +13,7 @@ const NavSidebarOption = ({
w="full"
fontWeight={isActive ? "bold" : "500"}
bgColor={isActive ? "gray.200" : "transparent"}
- _hover={{ bgColor: "gray.200", textDecoration: "none" }}
+ _hover={disableHoverEffect ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
justifyContent="start"
cursor="pointer"
borderRadius={4}
diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx
index faa2e4c..195fa0c 100644
--- a/app/src/components/nav/ProjectMenu.tsx
+++ b/app/src/components/nav/ProjectMenu.tsx
@@ -6,47 +6,78 @@ import {
PopoverTrigger,
PopoverContent,
Flex,
+ IconButton,
+ Icon,
+ Divider,
+ Button,
+ useDisclosure,
+ Spinner,
} from "@chakra-ui/react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import Link from "next/link";
+import { AiFillCaretDown } from "react-icons/ai";
+import { BsGear, BsPlus } from "react-icons/bs";
+import { type Organization } from "@prisma/client";
import { useAppStore } from "~/state/store";
import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption";
-import { useSelectedOrg } from "~/utils/hooks";
+import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
+import { useRouter } from "next/router";
export default function ProjectMenu() {
+ const router = useRouter();
+ const isActive = router.pathname.startsWith("/home");
+ const utils = api.useContext();
+
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
- const { data } = api.organizations.list.useQuery();
+ const { data: orgs } = api.organizations.list.useQuery();
useEffect(() => {
- if (data && data[0] && (!selectedOrgId || !data.find((org) => org.id === selectedOrgId))) {
- setSelectedOrgId(data[0].id);
+ if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
+ setSelectedOrgId(orgs[0].id);
}
- }, [selectedOrgId, setSelectedOrgId, data]);
+ }, [selectedOrgId, setSelectedOrgId, orgs]);
const { data: selectedOrg } = useSelectedOrg();
+ const [expandButtonHovered, setExpandButtonHovered] = useState(false);
+
+ const popover = useDisclosure();
+
+ const createMutation = api.organizations.create.useMutation();
+ const [createProject, isLoading] = useHandledAsyncCallback(async () => {
+ const newOrg = await createMutation.mutateAsync({ name: "New Project" });
+ await utils.organizations.list.invalidate();
+ setSelectedOrgId(newOrg.id);
+ await router.push({ pathname: "/settings" });
+ }, [createMutation, router]);
+
return (
<>
-
-
-
-
- PROJECT
-
-
-
-
+
+
+
+ PROJECT
+
+
+
+
+
-
-
+
+ }
+ size="xs"
+ colorScheme="gray"
+ color="gray.500"
+ variant="ghost"
+ mr={2}
+ borderRadius={4}
+ onMouseEnter={() => setExpandButtonHovered(true)}
+ onMouseLeave={() => setExpandButtonHovered(false)}
+ _hover={{ bgColor: isActive ? "gray.300" : "gray.200", transitionDelay: 0 }}
+ onClick={(event) => {
+ event.preventDefault();
+ popover.onToggle();
+ }}
+ />
+
+
+
+
+
+
+
+
+ PROJECTS
+
+
+
+ {orgs?.map((org) => (
+
+ ))}
+
+
+
+ New project
+
-
-
+
>
);
}
+
+const ProjectOption = ({ org, isActive }: { org: Organization; isActive: boolean }) => {
+ const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
+ const [gearHovered, setGearHovered] = useState(false);
+ return (
+ setSelectedOrgId(org.id)}
+ w="full"
+ justifyContent="space-between"
+ bgColor={isActive ? "gray.100" : "transparent"}
+ _hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
+ p={2}
+ borderRadius={4}
+ >
+ {org.name}
+ }
+ variant="ghost"
+ size="xs"
+ p={0}
+ onMouseEnter={() => setGearHovered(true)}
+ onMouseLeave={() => setGearHovered(false)}
+ _hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
+ borderRadius={4}
+ />
+
+ );
+};
diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts
index 1216f54..293e0f6 100644
--- a/app/src/server/api/routers/organizations.router.ts
+++ b/app/src/server/api/routers/organizations.router.ts
@@ -22,7 +22,7 @@ export const organizationsRouter = createTRPCRouter({
},
},
orderBy: {
- createdAt: "desc",
+ createdAt: "asc",
},
});
@@ -63,7 +63,7 @@ export const organizationsRouter = createTRPCRouter({
},
include: {
apiKeys: true,
- }
+ },
});
}),
update: protectedProcedure
@@ -79,4 +79,33 @@ export const organizationsRouter = createTRPCRouter({
},
});
}),
+ create: protectedProcedure
+ .input(z.object({ name: z.string() }))
+ .mutation(async ({ input, ctx }) => {
+ requireNothing(ctx);
+ const newOrgId = uuidv4();
+ const [newOrg] = await prisma.$transaction([
+ prisma.organization.create({
+ data: {
+ id: newOrgId,
+ name: input.name,
+ },
+ }),
+ prisma.organizationUser.create({
+ data: {
+ userId: ctx.session.user.id,
+ organizationId: newOrgId,
+ role: "ADMIN",
+ },
+ }),
+ prisma.apiKey.create({
+ data: {
+ name: "Default API Key",
+ organizationId: newOrgId,
+ apiKey: generateApiKey(),
+ },
+ }),
+ ]);
+ return newOrg;
+ }),
});