203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import {
|
|
HStack,
|
|
VStack,
|
|
Text,
|
|
Popover,
|
|
PopoverTrigger,
|
|
PopoverContent,
|
|
Flex,
|
|
IconButton,
|
|
Icon,
|
|
Divider,
|
|
Button,
|
|
useDisclosure,
|
|
Spinner,
|
|
} from "@chakra-ui/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 { 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: orgs } = api.organizations.list.useQuery();
|
|
|
|
useEffect(() => {
|
|
if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
|
|
setSelectedOrgId(orgs[0].id);
|
|
}
|
|
}, [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 (
|
|
<>
|
|
<Popover
|
|
placement="bottom-start"
|
|
isOpen={popover.isOpen}
|
|
onClose={popover.onClose}
|
|
closeOnBlur
|
|
>
|
|
<VStack w="full" alignItems="flex-start" spacing={0}>
|
|
<Text
|
|
pl={2}
|
|
pb={2}
|
|
fontSize="xs"
|
|
fontWeight="bold"
|
|
color="gray.500"
|
|
display={{ base: "none", md: "flex" }}
|
|
>
|
|
PROJECT
|
|
</Text>
|
|
<NavSidebarOption activeHrefPattern="/home" disableHoverEffect={expandButtonHovered}>
|
|
<Link href="/home">
|
|
<HStack w="full" justifyContent="space-between">
|
|
<HStack>
|
|
<Flex
|
|
p={1}
|
|
borderRadius={4}
|
|
backgroundColor="orange.100"
|
|
minW={{ base: 10, md: 8 }}
|
|
minH={{ base: 10, md: 8 }}
|
|
m={{ base: 0, md: 1 }}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
>
|
|
<Text>{selectedOrg?.name[0]?.toUpperCase()}</Text>
|
|
</Flex>
|
|
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1}>
|
|
{selectedOrg?.name}
|
|
</Text>
|
|
</HStack>
|
|
<PopoverTrigger>
|
|
<IconButton
|
|
aria-label="Open Project Menu"
|
|
icon={<Icon as={AiFillCaretDown} boxSize={3} />}
|
|
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();
|
|
}}
|
|
/>
|
|
</PopoverTrigger>
|
|
</HStack>
|
|
</Link>
|
|
</NavSidebarOption>
|
|
</VStack>
|
|
<PopoverContent
|
|
_focusVisible={{ boxShadow: "unset" }}
|
|
minW={0}
|
|
borderColor="blue.400"
|
|
w="auto"
|
|
>
|
|
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
|
|
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
|
|
PROJECTS
|
|
</Text>
|
|
<Divider />
|
|
<VStack spacing={0}>
|
|
{orgs?.map((org) => (
|
|
<ProjectOption
|
|
key={org.id}
|
|
org={org}
|
|
isActive={org.id === selectedOrgId}
|
|
onClose={popover.onClose}
|
|
/>
|
|
))}
|
|
</VStack>
|
|
<HStack
|
|
as={Button}
|
|
variant="ghost"
|
|
colorScheme="blue"
|
|
color="blue.400"
|
|
pr={8}
|
|
w="full"
|
|
onClick={createProject}
|
|
>
|
|
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
|
|
<Text>New project</Text>
|
|
</HStack>
|
|
</VStack>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const ProjectOption = ({
|
|
org,
|
|
isActive,
|
|
onClose,
|
|
}: {
|
|
org: Organization;
|
|
isActive: boolean;
|
|
onClose: () => void;
|
|
}) => {
|
|
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
|
|
const [gearHovered, setGearHovered] = useState(false);
|
|
return (
|
|
<HStack
|
|
as={Link}
|
|
href="/home"
|
|
onClick={() => {
|
|
setSelectedOrgId(org.id);
|
|
onClose();
|
|
}}
|
|
w="full"
|
|
justifyContent="space-between"
|
|
bgColor={isActive ? "gray.100" : "transparent"}
|
|
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
|
p={2}
|
|
borderRadius={4}
|
|
>
|
|
<Text>{org.name}</Text>
|
|
<IconButton
|
|
as={Link}
|
|
href="/settings"
|
|
aria-label={`Open ${org.name} settings`}
|
|
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
|
|
variant="ghost"
|
|
size="xs"
|
|
p={0}
|
|
onMouseEnter={() => setGearHovered(true)}
|
|
onMouseLeave={() => setGearHovered(false)}
|
|
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
|
|
borderRadius={4}
|
|
/>
|
|
</HStack>
|
|
);
|
|
};
|