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; + }), });