🛂 Migrate to Chakra UI v3 (#1496)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Alejandra
2025-02-17 19:33:00 +00:00
committed by GitHub
parent 74e2604faf
commit 55df823739
60 changed files with 4682 additions and 4399 deletions

View File

@@ -1,75 +0,0 @@
import {
Button,
Menu,
MenuButton,
MenuItem,
MenuList,
useDisclosure,
} from "@chakra-ui/react"
import { BsThreeDotsVertical } from "react-icons/bs"
import { FiEdit, FiTrash } from "react-icons/fi"
import type { ItemPublic, UserPublic } from "../../client"
import EditUser from "../Admin/EditUser"
import EditItem from "../Items/EditItem"
import Delete from "./DeleteAlert"
interface ActionsMenuProps {
type: string
value: ItemPublic | UserPublic
disabled?: boolean
}
const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => {
const editUserModal = useDisclosure()
const deleteModal = useDisclosure()
return (
<>
<Menu>
<MenuButton
isDisabled={disabled}
as={Button}
rightIcon={<BsThreeDotsVertical />}
variant="unstyled"
/>
<MenuList>
<MenuItem
onClick={editUserModal.onOpen}
icon={<FiEdit fontSize="16px" />}
>
Edit {type}
</MenuItem>
<MenuItem
onClick={deleteModal.onOpen}
icon={<FiTrash fontSize="16px" />}
color="ui.danger"
>
Delete {type}
</MenuItem>
</MenuList>
{type === "User" ? (
<EditUser
user={value as UserPublic}
isOpen={editUserModal.isOpen}
onClose={editUserModal.onClose}
/>
) : (
<EditItem
item={value as ItemPublic}
isOpen={editUserModal.isOpen}
onClose={editUserModal.onClose}
/>
)}
<Delete
type={type}
id={value.id}
isOpen={deleteModal.isOpen}
onClose={deleteModal.onClose}
/>
</Menu>
</>
)
}
export default ActionsMenu

View File

@@ -1,113 +0,0 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
} from "@chakra-ui/react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import React from "react"
import { useForm } from "react-hook-form"
import { ItemsService, UsersService } from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
interface DeleteProps {
type: string
id: string
isOpen: boolean
onClose: () => void
}
const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => {
const queryClient = useQueryClient()
const showToast = useCustomToast()
const cancelRef = React.useRef<HTMLButtonElement | null>(null)
const {
handleSubmit,
formState: { isSubmitting },
} = useForm()
const deleteEntity = async (id: string) => {
if (type === "Item") {
await ItemsService.deleteItem({ id: id })
} else if (type === "User") {
await UsersService.deleteUser({ userId: id })
} else {
throw new Error(`Unexpected type: ${type}`)
}
}
const mutation = useMutation({
mutationFn: deleteEntity,
onSuccess: () => {
showToast(
"Success",
`The ${type.toLowerCase()} was deleted successfully.`,
"success",
)
onClose()
},
onError: () => {
showToast(
"An error occurred.",
`An error occurred while deleting the ${type.toLowerCase()}.`,
"error",
)
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [type === "Item" ? "items" : "users"],
})
},
})
const onSubmit = async () => {
mutation.mutate(id)
}
return (
<>
<AlertDialog
isOpen={isOpen}
onClose={onClose}
leastDestructiveRef={cancelRef}
size={{ base: "sm", md: "md" }}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent as="form" onSubmit={handleSubmit(onSubmit)}>
<AlertDialogHeader>Delete {type}</AlertDialogHeader>
<AlertDialogBody>
{type === "User" && (
<span>
All items associated with this user will also be{" "}
<strong>permanently deleted. </strong>
</span>
)}
Are you sure? You will not be able to undo this action.
</AlertDialogBody>
<AlertDialogFooter gap={3}>
<Button variant="danger" type="submit" isLoading={isSubmitting}>
Delete
</Button>
<Button
ref={cancelRef}
onClick={onClose}
isDisabled={isSubmitting}
>
Cancel
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
)
}
export default Delete

View File

@@ -0,0 +1,27 @@
import { IconButton } from "@chakra-ui/react"
import { BsThreeDotsVertical } from "react-icons/bs"
import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu"
import type { ItemPublic } from "../../client"
import DeleteItem from "../Items/DeleteItem"
import EditItem from "../Items/EditItem"
interface ItemActionsMenuProps {
item: ItemPublic
}
export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
return (
<MenuRoot>
<MenuTrigger asChild>
<IconButton variant="ghost" color="inherit">
<BsThreeDotsVertical />
</IconButton>
</MenuTrigger>
<MenuContent>
<EditItem item={item} />
<DeleteItem id={item.id} />
</MenuContent>
</MenuRoot>
)
}

View File

@@ -1,38 +1,30 @@
import type { ComponentType, ElementType } from "react"
import { Flex, Image, useBreakpointValue } from "@chakra-ui/react"
import { Link } from "@tanstack/react-router"
import Logo from "/assets/images/fastapi-logo.svg"
import UserMenu from "./UserMenu"
import { Button, Flex, Icon, useDisclosure } from "@chakra-ui/react"
import { FaPlus } from "react-icons/fa"
function Navbar() {
const display = useBreakpointValue({ base: "none", md: "flex" })
interface NavbarProps {
type: string
addModalAs: ComponentType | ElementType
}
const Navbar = ({ type, addModalAs }: NavbarProps) => {
const addModal = useDisclosure()
const AddModal = addModalAs
return (
<>
<Flex py={8} gap={4}>
{/* TODO: Complete search functionality */}
{/* <InputGroup w={{ base: '100%', md: 'auto' }}>
<InputLeftElement pointerEvents='none'>
<Icon as={FaSearch} color='ui.dim' />
</InputLeftElement>
<Input type='text' placeholder='Search' fontSize={{ base: 'sm', md: 'inherit' }} borderRadius='8px' />
</InputGroup> */}
<Button
variant="primary"
gap={1}
fontSize={{ base: "sm", md: "inherit" }}
onClick={addModal.onOpen}
>
<Icon as={FaPlus} /> Add {type}
</Button>
<AddModal isOpen={addModal.isOpen} onClose={addModal.onClose} />
<Flex
display={display}
justify="space-between"
position="sticky"
color="white"
align="center"
bg="bg.muted"
w="100%"
top={0}
p={4}
>
<Link to="/">
<Image src={Logo} alt="Logo" w="180px" maxW="2xs" px={2} />
</Link>
<Flex gap={2} alignItems="center">
<UserMenu />
</Flex>
</>
</Flex>
)
}

View File

@@ -1,39 +1,55 @@
import { Button, Container, Text } from "@chakra-ui/react"
import { Button, Center, Flex, Text } from "@chakra-ui/react"
import { Link } from "@tanstack/react-router"
const NotFound = () => {
return (
<>
<Container
h="100vh"
alignItems="stretch"
justifyContent="center"
textAlign="center"
maxW="sm"
centerContent
<Flex
height="100vh"
align="center"
justify="center"
flexDir="column"
data-testid="not-found"
p={4}
>
<Flex alignItems="center" zIndex={1}>
<Flex flexDir="column" ml={4} align="center" justify="center" p={4}>
<Text
fontSize={{ base: "6xl", md: "8xl" }}
fontWeight="bold"
lineHeight="1"
mb={4}
>
404
</Text>
<Text fontSize="2xl" fontWeight="bold" mb={2}>
Oops!
</Text>
</Flex>
</Flex>
<Text
fontSize="8xl"
color="ui.main"
fontWeight="bold"
lineHeight="1"
fontSize="lg"
color="gray.600"
mb={4}
textAlign="center"
zIndex={1}
>
404
The page you are looking for was not found.
</Text>
<Text fontSize="md">Oops!</Text>
<Text fontSize="md">Page not found.</Text>
<Button
as={Link}
to="/"
color="ui.main"
borderColor="ui.main"
variant="outline"
mt={4}
>
Go back
</Button>
</Container>
<Center zIndex={1}>
<Link to="/">
<Button
variant="solid"
colorScheme="teal"
mt={4}
alignSelf="center"
>
Go Back
</Button>
</Link>
</Center>
</Flex>
</>
)
}

View File

@@ -1,36 +0,0 @@
import { Button, Flex } from "@chakra-ui/react"
type PaginationFooterProps = {
hasNextPage?: boolean
hasPreviousPage?: boolean
onChangePage: (newPage: number) => void
page: number
}
export function PaginationFooter({
hasNextPage,
hasPreviousPage,
onChangePage,
page,
}: PaginationFooterProps) {
return (
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button
onClick={() => onChangePage(page - 1)}
isDisabled={!hasPreviousPage || page <= 1}
>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => onChangePage(page + 1)}>
Next
</Button>
</Flex>
)
}

View File

@@ -1,33 +1,26 @@
import {
Box,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerOverlay,
Flex,
IconButton,
Image,
Text,
useColorModeValue,
useDisclosure,
} from "@chakra-ui/react"
import { Box, Flex, IconButton, Text } from "@chakra-ui/react"
import { useQueryClient } from "@tanstack/react-query"
import { FiLogOut, FiMenu } from "react-icons/fi"
import { useState } from "react"
import { FaBars } from "react-icons/fa"
import Logo from "/assets/images/fastapi-logo.svg"
import { FiLogOut } from "react-icons/fi"
import type { UserPublic } from "../../client"
import useAuth from "../../hooks/useAuth"
import {
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerRoot,
DrawerTrigger,
} from "../ui/drawer"
import SidebarItems from "./SidebarItems"
const Sidebar = () => {
const queryClient = useQueryClient()
const bgColor = useColorModeValue("ui.light", "ui.dark")
const textColor = useColorModeValue("ui.dark", "ui.light")
const secBgColor = useColorModeValue("ui.secondary", "ui.darkSlate")
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const { isOpen, onOpen, onClose } = useDisclosure()
const { logout } = useAuth()
const [open, setOpen] = useState(false)
const handleLogout = async () => {
logout()
@@ -36,78 +29,68 @@ const Sidebar = () => {
return (
<>
{/* Mobile */}
<IconButton
onClick={onOpen}
display={{ base: "flex", md: "none" }}
aria-label="Open Menu"
position="absolute"
fontSize="20px"
m={4}
icon={<FiMenu />}
/>
<Drawer isOpen={isOpen} placement="left" onClose={onClose}>
<DrawerOverlay />
<DrawerContent maxW="250px">
<DrawerCloseButton />
<DrawerBody py={8}>
<DrawerRoot
placement="start"
open={open}
onOpenChange={(e) => setOpen(e.open)}
>
<DrawerBackdrop />
<DrawerTrigger asChild>
<IconButton
variant="ghost"
color="inherit"
display={{ base: "flex", md: "none" }}
aria-label="Open Menu"
position="absolute"
zIndex="100"
m={4}
>
<FaBars />
</IconButton>
</DrawerTrigger>
<DrawerContent maxW="280px">
<DrawerCloseTrigger />
<DrawerBody>
<Flex flexDir="column" justify="space-between">
<Box>
<Image src={Logo} alt="logo" p={6} />
<SidebarItems onClose={onClose} />
<SidebarItems />
<Flex
as="button"
onClick={handleLogout}
p={2}
color="ui.danger"
fontWeight="bold"
alignItems="center"
gap={4}
px={4}
py={2}
>
<FiLogOut />
<Text ml={2}>Log out</Text>
<Text>Log Out</Text>
</Flex>
</Box>
{currentUser?.email && (
<Text color={textColor} noOfLines={2} fontSize="sm" p={2}>
<Text fontSize="sm" p={2}>
Logged in as: {currentUser.email}
</Text>
)}
</Flex>
</DrawerBody>
<DrawerCloseTrigger />
</DrawerContent>
</Drawer>
</DrawerRoot>
{/* Desktop */}
<Box
bg={bgColor}
p={3}
h="100vh"
position="sticky"
top="0"
display={{ base: "none", md: "flex" }}
position="sticky"
bg="bg.subtle"
top={0}
minW="280px"
h="100vh"
p={4}
>
<Flex
flexDir="column"
justify="space-between"
bg={secBgColor}
p={4}
borderRadius={12}
>
<Box>
<Image src={Logo} alt="Logo" w="180px" maxW="2xs" p={6} />
<SidebarItems />
</Box>
{currentUser?.email && (
<Text
color={textColor}
noOfLines={2}
fontSize="sm"
p={2}
maxW="180px"
>
Logged in as: {currentUser.email}
</Text>
)}
</Flex>
<Box w="100%">
<SidebarItems />
</Box>
</Box>
</>
)

View File

@@ -1,8 +1,9 @@
import { Box, Flex, Icon, Text, useColorModeValue } from "@chakra-ui/react"
import { Box, Flex, Icon, Text } from "@chakra-ui/react"
import { useQueryClient } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router"
import { FiBriefcase, FiHome, FiSettings, FiUsers } from "react-icons/fi"
import { Link as RouterLink } from "@tanstack/react-router"
import { FiBriefcase, FiHome, FiSettings, FiUsers } from "react-icons/fi"
import type { IconType } from "react-icons/lib"
import type { UserPublic } from "../../client"
const items = [
@@ -15,39 +16,43 @@ interface SidebarItemsProps {
onClose?: () => void
}
interface Item {
icon: IconType
title: string
path: string
}
const SidebarItems = ({ onClose }: SidebarItemsProps) => {
const queryClient = useQueryClient()
const textColor = useColorModeValue("ui.main", "ui.light")
const bgActive = useColorModeValue("#E2E8F0", "#4A5568")
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const finalItems = currentUser?.is_superuser
const finalItems: Item[] = currentUser?.is_superuser
? [...items, { icon: FiUsers, title: "Admin", path: "/admin" }]
: items
const listItems = finalItems.map(({ icon, title, path }) => (
<Flex
as={Link}
to={path}
w="100%"
p={2}
key={title}
activeProps={{
style: {
background: bgActive,
borderRadius: "12px",
},
}}
color={textColor}
onClick={onClose}
>
<Icon as={icon} alignSelf="center" />
<Text ml={2}>{title}</Text>
</Flex>
<RouterLink key={title} to={path} onClick={onClose}>
<Flex
gap={4}
px={4}
py={2}
_hover={{
background: "gray.subtle",
}}
alignItems="center"
fontSize="sm"
>
<Icon as={icon} alignSelf="center" />
<Text ml={2}>{title}</Text>
</Flex>
</RouterLink>
))
return (
<>
<Text fontSize="xs" px={4} py={2} fontWeight="bold">
Menu
</Text>
<Box>{listItems}</Box>
</>
)

View File

@@ -0,0 +1,28 @@
import { IconButton } from "@chakra-ui/react"
import { BsThreeDotsVertical } from "react-icons/bs"
import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu"
import type { UserPublic } from "../../client"
import DeleteUser from "../Admin/DeleteUser"
import EditUser from "../Admin/EditUser"
interface UserActionsMenuProps {
user: UserPublic
disabled?: boolean
}
export const UserActionsMenu = ({ user, disabled }: UserActionsMenuProps) => {
return (
<MenuRoot>
<MenuTrigger asChild>
<IconButton variant="ghost" color="inherit" disabled={disabled}>
<BsThreeDotsVertical />
</IconButton>
</MenuTrigger>
<MenuContent>
<EditUser user={user} />
<DeleteUser id={user.id} />
</MenuContent>
</MenuRoot>
)
}

View File

@@ -1,19 +1,13 @@
import {
Box,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
} from "@chakra-ui/react"
import { Box, Button, Flex, Text } from "@chakra-ui/react"
import { Link } from "@tanstack/react-router"
import { FaUserAstronaut } from "react-icons/fa"
import { FiLogOut, FiUser } from "react-icons/fi"
import { FiLogOut, FiUser } from "react-icons/fi"
import useAuth from "../../hooks/useAuth"
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from "../ui/menu"
const UserMenu = () => {
const { logout } = useAuth()
const { user, logout } = useAuth()
const handleLogout = async () => {
logout()
@@ -22,36 +16,47 @@ const UserMenu = () => {
return (
<>
{/* Desktop */}
<Box
display={{ base: "none", md: "block" }}
position="fixed"
top={4}
right={4}
>
<Menu>
<MenuButton
as={IconButton}
aria-label="Options"
icon={<FaUserAstronaut color="white" fontSize="18px" />}
bg="ui.main"
isRound
data-testid="user-menu"
/>
<MenuList>
<MenuItem icon={<FiUser fontSize="18px" />} as={Link} to="settings">
My profile
</MenuItem>
<MenuItem
icon={<FiLogOut fontSize="18px" />}
onClick={handleLogout}
color="ui.danger"
fontWeight="bold"
<Flex>
<MenuRoot>
<MenuTrigger asChild p={2}>
<Button
data-testid="user-menu"
variant="solid"
maxW="150px"
truncate
>
Log out
<FaUserAstronaut fontSize="18" />
<Text>{user?.full_name || "User"}</Text>
</Button>
</MenuTrigger>
<MenuContent>
<Link to="settings">
<MenuItem
closeOnSelect
value="user-settings"
gap={2}
py={2}
style={{ cursor: "pointer" }}
>
<FiUser fontSize="18px" />
<Box flex="1">My Profile</Box>
</MenuItem>
</Link>
<MenuItem
value="logout"
gap={2}
py={2}
onClick={handleLogout}
style={{ cursor: "pointer" }}
>
<FiLogOut />
Log Out
</MenuItem>
</MenuList>
</Menu>
</Box>
</MenuContent>
</MenuRoot>
</Flex>
</>
)
}