committed by
GitHub
60 changed files with 6712 additions and 6429 deletions
File diff suppressed because it is too large
@ -0,0 +1,103 @@ |
|||||
|
import { Button, DialogTitle, Text } from "@chakra-ui/react" |
||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query" |
||||
|
import { useState } from "react" |
||||
|
import { useForm } from "react-hook-form" |
||||
|
import { FiTrash2 } from "react-icons/fi" |
||||
|
import { UsersService } from "../../client" |
||||
|
import { |
||||
|
DialogActionTrigger, |
||||
|
DialogBody, |
||||
|
DialogCloseTrigger, |
||||
|
DialogContent, |
||||
|
DialogFooter, |
||||
|
DialogHeader, |
||||
|
DialogRoot, |
||||
|
DialogTrigger, |
||||
|
} from "../../components/ui/dialog" |
||||
|
import useCustomToast from "../../hooks/useCustomToast" |
||||
|
|
||||
|
const DeleteUser = ({ id }: { id: string }) => { |
||||
|
const [isOpen, setIsOpen] = useState(false) |
||||
|
const queryClient = useQueryClient() |
||||
|
const { showSuccessToast, showErrorToast } = useCustomToast() |
||||
|
const { |
||||
|
handleSubmit, |
||||
|
formState: { isSubmitting }, |
||||
|
} = useForm() |
||||
|
|
||||
|
const deleteUser = async (id: string) => { |
||||
|
await UsersService.deleteUser({ userId: id }) |
||||
|
} |
||||
|
|
||||
|
const mutation = useMutation({ |
||||
|
mutationFn: deleteUser, |
||||
|
onSuccess: () => { |
||||
|
showSuccessToast("The user was deleted successfully") |
||||
|
setIsOpen(false) |
||||
|
}, |
||||
|
onError: () => { |
||||
|
showErrorToast("An error occurred while deleting the user") |
||||
|
}, |
||||
|
onSettled: () => { |
||||
|
queryClient.invalidateQueries() |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
const onSubmit = async () => { |
||||
|
mutation.mutate(id) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<DialogRoot |
||||
|
size={{ base: "xs", md: "md" }} |
||||
|
placement="center" |
||||
|
role="alertdialog" |
||||
|
open={isOpen} |
||||
|
onOpenChange={({ open }) => setIsOpen(open)} |
||||
|
> |
||||
|
<DialogTrigger asChild> |
||||
|
<Button variant="ghost" size="sm" colorPalette="red"> |
||||
|
<FiTrash2 fontSize="16px" /> |
||||
|
Delete User |
||||
|
</Button> |
||||
|
</DialogTrigger> |
||||
|
<DialogContent> |
||||
|
<form onSubmit={handleSubmit(onSubmit)}> |
||||
|
<DialogHeader> |
||||
|
<DialogTitle>Delete User</DialogTitle> |
||||
|
</DialogHeader> |
||||
|
<DialogBody> |
||||
|
<Text mb={4}> |
||||
|
All items associated with this user will also be{" "} |
||||
|
<strong>permanently deleted.</strong> Are you sure? You will not |
||||
|
be able to undo this action. |
||||
|
</Text> |
||||
|
</DialogBody> |
||||
|
|
||||
|
<DialogFooter gap={2}> |
||||
|
<DialogActionTrigger asChild> |
||||
|
<Button |
||||
|
variant="subtle" |
||||
|
colorPalette="gray" |
||||
|
disabled={isSubmitting} |
||||
|
> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</DialogActionTrigger> |
||||
|
<Button |
||||
|
variant="solid" |
||||
|
colorPalette="red" |
||||
|
type="submit" |
||||
|
loading={isSubmitting} |
||||
|
> |
||||
|
Delete |
||||
|
</Button> |
||||
|
</DialogFooter> |
||||
|
<DialogCloseTrigger /> |
||||
|
</form> |
||||
|
</DialogContent> |
||||
|
</DialogRoot> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default DeleteUser |
@ -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 |
|
@ -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 |
|
@ -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> |
||||
|
) |
||||
|
} |
@ -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> |
|
||||
) |
|
||||
} |
|
@ -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> |
||||
|
) |
||||
|
} |
@ -0,0 +1,103 @@ |
|||||
|
import { Button, DialogTitle, Text } from "@chakra-ui/react" |
||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query" |
||||
|
import { useState } from "react" |
||||
|
import { useForm } from "react-hook-form" |
||||
|
import { FiTrash2 } from "react-icons/fi" |
||||
|
import { ItemsService } from "../../client" |
||||
|
import { |
||||
|
DialogActionTrigger, |
||||
|
DialogBody, |
||||
|
DialogCloseTrigger, |
||||
|
DialogContent, |
||||
|
DialogFooter, |
||||
|
DialogHeader, |
||||
|
DialogRoot, |
||||
|
DialogTrigger, |
||||
|
} from "../../components/ui/dialog" |
||||
|
import useCustomToast from "../../hooks/useCustomToast" |
||||
|
|
||||
|
const DeleteItem = ({ id }: { id: string }) => { |
||||
|
const [isOpen, setIsOpen] = useState(false) |
||||
|
const queryClient = useQueryClient() |
||||
|
const { showSuccessToast, showErrorToast } = useCustomToast() |
||||
|
const { |
||||
|
handleSubmit, |
||||
|
formState: { isSubmitting }, |
||||
|
} = useForm() |
||||
|
|
||||
|
const deleteItem = async (id: string) => { |
||||
|
await ItemsService.deleteItem({ id: id }) |
||||
|
} |
||||
|
|
||||
|
const mutation = useMutation({ |
||||
|
mutationFn: deleteItem, |
||||
|
onSuccess: () => { |
||||
|
showSuccessToast("The item was deleted successfully") |
||||
|
setIsOpen(false) |
||||
|
}, |
||||
|
onError: () => { |
||||
|
showErrorToast("An error occurred while deleting the item") |
||||
|
}, |
||||
|
onSettled: () => { |
||||
|
queryClient.invalidateQueries() |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
const onSubmit = async () => { |
||||
|
mutation.mutate(id) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<DialogRoot |
||||
|
size={{ base: "xs", md: "md" }} |
||||
|
placement="center" |
||||
|
role="alertdialog" |
||||
|
open={isOpen} |
||||
|
onOpenChange={({ open }) => setIsOpen(open)} |
||||
|
> |
||||
|
<DialogTrigger asChild> |
||||
|
<Button variant="ghost" size="sm" colorPalette="red"> |
||||
|
<FiTrash2 fontSize="16px" /> |
||||
|
Delete Item |
||||
|
</Button> |
||||
|
</DialogTrigger> |
||||
|
|
||||
|
<DialogContent> |
||||
|
<form onSubmit={handleSubmit(onSubmit)}> |
||||
|
<DialogCloseTrigger /> |
||||
|
<DialogHeader> |
||||
|
<DialogTitle>Delete Item</DialogTitle> |
||||
|
</DialogHeader> |
||||
|
<DialogBody> |
||||
|
<Text mb={4}> |
||||
|
This item will be permanently deleted. Are you sure? You will not |
||||
|
be able to undo this action. |
||||
|
</Text> |
||||
|
</DialogBody> |
||||
|
|
||||
|
<DialogFooter gap={2}> |
||||
|
<DialogActionTrigger asChild> |
||||
|
<Button |
||||
|
variant="subtle" |
||||
|
colorPalette="gray" |
||||
|
disabled={isSubmitting} |
||||
|
> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</DialogActionTrigger> |
||||
|
<Button |
||||
|
variant="solid" |
||||
|
colorPalette="red" |
||||
|
type="submit" |
||||
|
loading={isSubmitting} |
||||
|
> |
||||
|
Delete |
||||
|
</Button> |
||||
|
</DialogFooter> |
||||
|
</form> |
||||
|
</DialogContent> |
||||
|
</DialogRoot> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default DeleteItem |
@ -0,0 +1,34 @@ |
|||||
|
import { Skeleton, Table } from "@chakra-ui/react" |
||||
|
|
||||
|
const PendingItems = () => ( |
||||
|
<Table.Root size={{ base: "sm", md: "md" }}> |
||||
|
<Table.Header> |
||||
|
<Table.Row> |
||||
|
<Table.ColumnHeader w="30%">ID</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="30%">Title</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="30%">Description</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="10%">Actions</Table.ColumnHeader> |
||||
|
</Table.Row> |
||||
|
</Table.Header> |
||||
|
<Table.Body> |
||||
|
{[...Array(5)].map((_, index) => ( |
||||
|
<Table.Row key={index}> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
</Table.Row> |
||||
|
))} |
||||
|
</Table.Body> |
||||
|
</Table.Root> |
||||
|
) |
||||
|
|
||||
|
export default PendingItems |
@ -0,0 +1,38 @@ |
|||||
|
import { Skeleton, Table } from "@chakra-ui/react" |
||||
|
|
||||
|
const PendingUsers = () => ( |
||||
|
<Table.Root size={{ base: "sm", md: "md" }}> |
||||
|
<Table.Header> |
||||
|
<Table.Row> |
||||
|
<Table.ColumnHeader w="20%">Full name</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="25%">Email</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="15%">Role</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="20%">Status</Table.ColumnHeader> |
||||
|
<Table.ColumnHeader w="20%">Actions</Table.ColumnHeader> |
||||
|
</Table.Row> |
||||
|
</Table.Header> |
||||
|
<Table.Body> |
||||
|
{[...Array(5)].map((_, index) => ( |
||||
|
<Table.Row key={index}> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
<Table.Cell> |
||||
|
<Skeleton h="20px" /> |
||||
|
</Table.Cell> |
||||
|
</Table.Row> |
||||
|
))} |
||||
|
</Table.Body> |
||||
|
</Table.Root> |
||||
|
) |
||||
|
|
||||
|
export default PendingUsers |
@ -1,35 +1,19 @@ |
|||||
import { |
import { Container, Heading, Text } from "@chakra-ui/react" |
||||
Button, |
|
||||
Container, |
|
||||
Heading, |
|
||||
Text, |
|
||||
useDisclosure, |
|
||||
} from "@chakra-ui/react" |
|
||||
|
|
||||
import DeleteConfirmation from "./DeleteConfirmation" |
import DeleteConfirmation from "./DeleteConfirmation" |
||||
|
|
||||
const DeleteAccount = () => { |
const DeleteAccount = () => { |
||||
const confirmationModal = useDisclosure() |
|
||||
|
|
||||
return ( |
return ( |
||||
<> |
<Container maxW="full"> |
||||
<Container maxW="full"> |
<Heading size="sm" py={4}> |
||||
<Heading size="sm" py={4}> |
Delete Account |
||||
Delete Account |
</Heading> |
||||
</Heading> |
<Text> |
||||
<Text> |
Permanently delete your data and everything associated with your |
||||
Permanently delete your data and everything associated with your |
account. |
||||
account. |
</Text> |
||||
</Text> |
<DeleteConfirmation /> |
||||
<Button variant="danger" mt={4} onClick={confirmationModal.onOpen}> |
</Container> |
||||
Delete |
|
||||
</Button> |
|
||||
<DeleteConfirmation |
|
||||
isOpen={confirmationModal.isOpen} |
|
||||
onClose={confirmationModal.onClose} |
|
||||
/> |
|
||||
</Container> |
|
||||
</> |
|
||||
) |
) |
||||
} |
} |
||||
export default DeleteAccount |
export default DeleteAccount |
||||
|
@ -0,0 +1,40 @@ |
|||||
|
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" |
||||
|
import { |
||||
|
AbsoluteCenter, |
||||
|
Button as ChakraButton, |
||||
|
Span, |
||||
|
Spinner, |
||||
|
} from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
interface ButtonLoadingProps { |
||||
|
loading?: boolean |
||||
|
loadingText?: React.ReactNode |
||||
|
} |
||||
|
|
||||
|
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} |
||||
|
|
||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( |
||||
|
function Button(props, ref) { |
||||
|
const { loading, disabled, loadingText, children, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}> |
||||
|
{loading && !loadingText ? ( |
||||
|
<> |
||||
|
<AbsoluteCenter display="inline-flex"> |
||||
|
<Spinner size="inherit" color="inherit" /> |
||||
|
</AbsoluteCenter> |
||||
|
<Span opacity={0}>{children}</Span> |
||||
|
</> |
||||
|
) : loading && loadingText ? ( |
||||
|
<> |
||||
|
<Spinner size="inherit" color="inherit" /> |
||||
|
{loadingText} |
||||
|
</> |
||||
|
) : ( |
||||
|
children |
||||
|
)} |
||||
|
</ChakraButton> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,25 @@ |
|||||
|
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
export interface CheckboxProps extends ChakraCheckbox.RootProps { |
||||
|
icon?: React.ReactNode |
||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement> |
||||
|
rootRef?: React.Ref<HTMLLabelElement> |
||||
|
} |
||||
|
|
||||
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( |
||||
|
function Checkbox(props, ref) { |
||||
|
const { icon, children, inputProps, rootRef, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraCheckbox.Root ref={rootRef} {...rest}> |
||||
|
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} /> |
||||
|
<ChakraCheckbox.Control> |
||||
|
{icon || <ChakraCheckbox.Indicator />} |
||||
|
</ChakraCheckbox.Control> |
||||
|
{children != null && ( |
||||
|
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label> |
||||
|
)} |
||||
|
</ChakraCheckbox.Root> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,17 @@ |
|||||
|
import type { ButtonProps } from "@chakra-ui/react" |
||||
|
import { IconButton as ChakraIconButton } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
import { LuX } from "react-icons/lu" |
||||
|
|
||||
|
export type CloseButtonProps = ButtonProps |
||||
|
|
||||
|
export const CloseButton = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
CloseButtonProps |
||||
|
>(function CloseButton(props, ref) { |
||||
|
return ( |
||||
|
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}> |
||||
|
{props.children ?? <LuX />} |
||||
|
</ChakraIconButton> |
||||
|
) |
||||
|
}) |
@ -0,0 +1,107 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import type { IconButtonProps, SpanProps } from "@chakra-ui/react" |
||||
|
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react" |
||||
|
import { ThemeProvider, useTheme } from "next-themes" |
||||
|
import type { ThemeProviderProps } from "next-themes" |
||||
|
import * as React from "react" |
||||
|
import { LuMoon, LuSun } from "react-icons/lu" |
||||
|
|
||||
|
export interface ColorModeProviderProps extends ThemeProviderProps {} |
||||
|
|
||||
|
export function ColorModeProvider(props: ColorModeProviderProps) { |
||||
|
return ( |
||||
|
<ThemeProvider attribute="class" disableTransitionOnChange {...props} /> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export type ColorMode = "light" | "dark" |
||||
|
|
||||
|
export interface UseColorModeReturn { |
||||
|
colorMode: ColorMode |
||||
|
setColorMode: (colorMode: ColorMode) => void |
||||
|
toggleColorMode: () => void |
||||
|
} |
||||
|
|
||||
|
export function useColorMode(): UseColorModeReturn { |
||||
|
const { resolvedTheme, setTheme } = useTheme() |
||||
|
const toggleColorMode = () => { |
||||
|
setTheme(resolvedTheme === "dark" ? "light" : "dark") |
||||
|
} |
||||
|
return { |
||||
|
colorMode: resolvedTheme as ColorMode, |
||||
|
setColorMode: setTheme, |
||||
|
toggleColorMode, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function useColorModeValue<T>(light: T, dark: T) { |
||||
|
const { colorMode } = useColorMode() |
||||
|
return colorMode === "dark" ? dark : light |
||||
|
} |
||||
|
|
||||
|
export function ColorModeIcon() { |
||||
|
const { colorMode } = useColorMode() |
||||
|
return colorMode === "dark" ? <LuMoon /> : <LuSun /> |
||||
|
} |
||||
|
|
||||
|
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {} |
||||
|
|
||||
|
export const ColorModeButton = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ColorModeButtonProps |
||||
|
>(function ColorModeButton(props, ref) { |
||||
|
const { toggleColorMode } = useColorMode() |
||||
|
return ( |
||||
|
<ClientOnly fallback={<Skeleton boxSize="8" />}> |
||||
|
<IconButton |
||||
|
onClick={toggleColorMode} |
||||
|
variant="ghost" |
||||
|
aria-label="Toggle color mode" |
||||
|
size="sm" |
||||
|
ref={ref} |
||||
|
{...props} |
||||
|
css={{ |
||||
|
_icon: { |
||||
|
width: "5", |
||||
|
height: "5", |
||||
|
}, |
||||
|
}} |
||||
|
> |
||||
|
<ColorModeIcon /> |
||||
|
</IconButton> |
||||
|
</ClientOnly> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>( |
||||
|
function LightMode(props, ref) { |
||||
|
return ( |
||||
|
<Span |
||||
|
color="fg" |
||||
|
display="contents" |
||||
|
className="chakra-theme light" |
||||
|
colorPalette="gray" |
||||
|
colorScheme="light" |
||||
|
ref={ref} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>( |
||||
|
function DarkMode(props, ref) { |
||||
|
return ( |
||||
|
<Span |
||||
|
color="fg" |
||||
|
display="contents" |
||||
|
className="chakra-theme dark" |
||||
|
colorPalette="gray" |
||||
|
colorScheme="dark" |
||||
|
ref={ref} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,62 @@ |
|||||
|
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
import { CloseButton } from "./close-button" |
||||
|
|
||||
|
interface DialogContentProps extends ChakraDialog.ContentProps { |
||||
|
portalled?: boolean |
||||
|
portalRef?: React.RefObject<HTMLElement> |
||||
|
backdrop?: boolean |
||||
|
} |
||||
|
|
||||
|
export const DialogContent = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
DialogContentProps |
||||
|
>(function DialogContent(props, ref) { |
||||
|
const { |
||||
|
children, |
||||
|
portalled = true, |
||||
|
portalRef, |
||||
|
backdrop = true, |
||||
|
...rest |
||||
|
} = props |
||||
|
|
||||
|
return ( |
||||
|
<Portal disabled={!portalled} container={portalRef}> |
||||
|
{backdrop && <ChakraDialog.Backdrop />} |
||||
|
<ChakraDialog.Positioner> |
||||
|
<ChakraDialog.Content ref={ref} {...rest} asChild={false}> |
||||
|
{children} |
||||
|
</ChakraDialog.Content> |
||||
|
</ChakraDialog.Positioner> |
||||
|
</Portal> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const DialogCloseTrigger = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ChakraDialog.CloseTriggerProps |
||||
|
>(function DialogCloseTrigger(props, ref) { |
||||
|
return ( |
||||
|
<ChakraDialog.CloseTrigger |
||||
|
position="absolute" |
||||
|
top="2" |
||||
|
insetEnd="2" |
||||
|
{...props} |
||||
|
asChild |
||||
|
> |
||||
|
<CloseButton size="sm" ref={ref}> |
||||
|
{props.children} |
||||
|
</CloseButton> |
||||
|
</ChakraDialog.CloseTrigger> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const DialogRoot = ChakraDialog.Root |
||||
|
export const DialogFooter = ChakraDialog.Footer |
||||
|
export const DialogHeader = ChakraDialog.Header |
||||
|
export const DialogBody = ChakraDialog.Body |
||||
|
export const DialogBackdrop = ChakraDialog.Backdrop |
||||
|
export const DialogTitle = ChakraDialog.Title |
||||
|
export const DialogDescription = ChakraDialog.Description |
||||
|
export const DialogTrigger = ChakraDialog.Trigger |
||||
|
export const DialogActionTrigger = ChakraDialog.ActionTrigger |
@ -0,0 +1,52 @@ |
|||||
|
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
import { CloseButton } from "./close-button" |
||||
|
|
||||
|
interface DrawerContentProps extends ChakraDrawer.ContentProps { |
||||
|
portalled?: boolean |
||||
|
portalRef?: React.RefObject<HTMLElement> |
||||
|
offset?: ChakraDrawer.ContentProps["padding"] |
||||
|
} |
||||
|
|
||||
|
export const DrawerContent = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
DrawerContentProps |
||||
|
>(function DrawerContent(props, ref) { |
||||
|
const { children, portalled = true, portalRef, offset, ...rest } = props |
||||
|
return ( |
||||
|
<Portal disabled={!portalled} container={portalRef}> |
||||
|
<ChakraDrawer.Positioner padding={offset}> |
||||
|
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}> |
||||
|
{children} |
||||
|
</ChakraDrawer.Content> |
||||
|
</ChakraDrawer.Positioner> |
||||
|
</Portal> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const DrawerCloseTrigger = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ChakraDrawer.CloseTriggerProps |
||||
|
>(function DrawerCloseTrigger(props, ref) { |
||||
|
return ( |
||||
|
<ChakraDrawer.CloseTrigger |
||||
|
position="absolute" |
||||
|
top="2" |
||||
|
insetEnd="2" |
||||
|
{...props} |
||||
|
asChild |
||||
|
> |
||||
|
<CloseButton size="sm" ref={ref} /> |
||||
|
</ChakraDrawer.CloseTrigger> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const DrawerTrigger = ChakraDrawer.Trigger |
||||
|
export const DrawerRoot = ChakraDrawer.Root |
||||
|
export const DrawerFooter = ChakraDrawer.Footer |
||||
|
export const DrawerHeader = ChakraDrawer.Header |
||||
|
export const DrawerBody = ChakraDrawer.Body |
||||
|
export const DrawerBackdrop = ChakraDrawer.Backdrop |
||||
|
export const DrawerDescription = ChakraDrawer.Description |
||||
|
export const DrawerTitle = ChakraDrawer.Title |
||||
|
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger |
@ -0,0 +1,33 @@ |
|||||
|
import { Field as ChakraField } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> { |
||||
|
label?: React.ReactNode |
||||
|
helperText?: React.ReactNode |
||||
|
errorText?: React.ReactNode |
||||
|
optionalText?: React.ReactNode |
||||
|
} |
||||
|
|
||||
|
export const Field = React.forwardRef<HTMLDivElement, FieldProps>( |
||||
|
function Field(props, ref) { |
||||
|
const { label, children, helperText, errorText, optionalText, ...rest } = |
||||
|
props |
||||
|
return ( |
||||
|
<ChakraField.Root ref={ref} {...rest}> |
||||
|
{label && ( |
||||
|
<ChakraField.Label> |
||||
|
{label} |
||||
|
<ChakraField.RequiredIndicator fallback={optionalText} /> |
||||
|
</ChakraField.Label> |
||||
|
)} |
||||
|
{children} |
||||
|
{helperText && ( |
||||
|
<ChakraField.HelperText>{helperText}</ChakraField.HelperText> |
||||
|
)} |
||||
|
{errorText && ( |
||||
|
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText> |
||||
|
)} |
||||
|
</ChakraField.Root> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,53 @@ |
|||||
|
import type { BoxProps, InputElementProps } from "@chakra-ui/react" |
||||
|
import { Group, InputElement } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
export interface InputGroupProps extends BoxProps { |
||||
|
startElementProps?: InputElementProps |
||||
|
endElementProps?: InputElementProps |
||||
|
startElement?: React.ReactNode |
||||
|
endElement?: React.ReactNode |
||||
|
children: React.ReactElement<InputElementProps> |
||||
|
startOffset?: InputElementProps["paddingStart"] |
||||
|
endOffset?: InputElementProps["paddingEnd"] |
||||
|
} |
||||
|
|
||||
|
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>( |
||||
|
function InputGroup(props, ref) { |
||||
|
const { |
||||
|
startElement, |
||||
|
startElementProps, |
||||
|
endElement, |
||||
|
endElementProps, |
||||
|
children, |
||||
|
startOffset = "6px", |
||||
|
endOffset = "6px", |
||||
|
...rest |
||||
|
} = props |
||||
|
|
||||
|
const child = |
||||
|
React.Children.only<React.ReactElement<InputElementProps>>(children) |
||||
|
|
||||
|
return ( |
||||
|
<Group ref={ref} {...rest}> |
||||
|
{startElement && ( |
||||
|
<InputElement pointerEvents="none" {...startElementProps}> |
||||
|
{startElement} |
||||
|
</InputElement> |
||||
|
)} |
||||
|
{React.cloneElement(child, { |
||||
|
...(startElement && { |
||||
|
ps: `calc(var(--input-height) - ${startOffset})`, |
||||
|
}), |
||||
|
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), |
||||
|
...children.props, |
||||
|
})} |
||||
|
{endElement && ( |
||||
|
<InputElement placement="end" {...endElementProps}> |
||||
|
{endElement} |
||||
|
</InputElement> |
||||
|
)} |
||||
|
</Group> |
||||
|
) |
||||
|
}, |
||||
|
) |
@ -0,0 +1,12 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react" |
||||
|
import { createRecipeContext } from "@chakra-ui/react" |
||||
|
|
||||
|
export interface LinkButtonProps |
||||
|
extends HTMLChakraProps<"a", RecipeProps<"button">> {} |
||||
|
|
||||
|
const { withContext } = createRecipeContext({ key: "button" }) |
||||
|
|
||||
|
// Replace "a" with your framework's link component
|
||||
|
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>("a") |
@ -0,0 +1,112 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
import { LuCheck, LuChevronRight } from "react-icons/lu" |
||||
|
|
||||
|
interface MenuContentProps extends ChakraMenu.ContentProps { |
||||
|
portalled?: boolean |
||||
|
portalRef?: React.RefObject<HTMLElement> |
||||
|
} |
||||
|
|
||||
|
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>( |
||||
|
function MenuContent(props, ref) { |
||||
|
const { portalled = true, portalRef, ...rest } = props |
||||
|
return ( |
||||
|
<Portal disabled={!portalled} container={portalRef}> |
||||
|
<ChakraMenu.Positioner> |
||||
|
<ChakraMenu.Content ref={ref} {...rest} /> |
||||
|
</ChakraMenu.Positioner> |
||||
|
</Portal> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
export const MenuArrow = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
ChakraMenu.ArrowProps |
||||
|
>(function MenuArrow(props, ref) { |
||||
|
return ( |
||||
|
<ChakraMenu.Arrow ref={ref} {...props}> |
||||
|
<ChakraMenu.ArrowTip /> |
||||
|
</ChakraMenu.Arrow> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const MenuCheckboxItem = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
ChakraMenu.CheckboxItemProps |
||||
|
>(function MenuCheckboxItem(props, ref) { |
||||
|
return ( |
||||
|
<ChakraMenu.CheckboxItem ps="8" ref={ref} {...props}> |
||||
|
<AbsoluteCenter axis="horizontal" insetStart="4" asChild> |
||||
|
<ChakraMenu.ItemIndicator> |
||||
|
<LuCheck /> |
||||
|
</ChakraMenu.ItemIndicator> |
||||
|
</AbsoluteCenter> |
||||
|
{props.children} |
||||
|
</ChakraMenu.CheckboxItem> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const MenuRadioItem = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
ChakraMenu.RadioItemProps |
||||
|
>(function MenuRadioItem(props, ref) { |
||||
|
const { children, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}> |
||||
|
<AbsoluteCenter axis="horizontal" insetStart="4" asChild> |
||||
|
<ChakraMenu.ItemIndicator> |
||||
|
<LuCheck /> |
||||
|
</ChakraMenu.ItemIndicator> |
||||
|
</AbsoluteCenter> |
||||
|
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText> |
||||
|
</ChakraMenu.RadioItem> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const MenuItemGroup = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
ChakraMenu.ItemGroupProps |
||||
|
>(function MenuItemGroup(props, ref) { |
||||
|
const { title, children, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraMenu.ItemGroup ref={ref} {...rest}> |
||||
|
{title && ( |
||||
|
<ChakraMenu.ItemGroupLabel userSelect="none"> |
||||
|
{title} |
||||
|
</ChakraMenu.ItemGroupLabel> |
||||
|
)} |
||||
|
{children} |
||||
|
</ChakraMenu.ItemGroup> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps { |
||||
|
startIcon?: React.ReactNode |
||||
|
} |
||||
|
|
||||
|
export const MenuTriggerItem = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
MenuTriggerItemProps |
||||
|
>(function MenuTriggerItem(props, ref) { |
||||
|
const { startIcon, children, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraMenu.TriggerItem ref={ref} {...rest}> |
||||
|
{startIcon} |
||||
|
{children} |
||||
|
<LuChevronRight /> |
||||
|
</ChakraMenu.TriggerItem> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup |
||||
|
export const MenuContextTrigger = ChakraMenu.ContextTrigger |
||||
|
export const MenuRoot = ChakraMenu.Root |
||||
|
export const MenuSeparator = ChakraMenu.Separator |
||||
|
|
||||
|
export const MenuItem = ChakraMenu.Item |
||||
|
export const MenuItemText = ChakraMenu.ItemText |
||||
|
export const MenuItemCommand = ChakraMenu.ItemCommand |
||||
|
export const MenuTrigger = ChakraMenu.Trigger |
@ -0,0 +1,211 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import type { ButtonProps, TextProps } from "@chakra-ui/react" |
||||
|
import { |
||||
|
Button, |
||||
|
Pagination as ChakraPagination, |
||||
|
IconButton, |
||||
|
Text, |
||||
|
createContext, |
||||
|
usePaginationContext, |
||||
|
} from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
import { |
||||
|
HiChevronLeft, |
||||
|
HiChevronRight, |
||||
|
HiMiniEllipsisHorizontal, |
||||
|
} from "react-icons/hi2" |
||||
|
import { LinkButton } from "./link-button" |
||||
|
|
||||
|
interface ButtonVariantMap { |
||||
|
current: ButtonProps["variant"] |
||||
|
default: ButtonProps["variant"] |
||||
|
ellipsis: ButtonProps["variant"] |
||||
|
} |
||||
|
|
||||
|
type PaginationVariant = "outline" | "solid" | "subtle" |
||||
|
|
||||
|
interface ButtonVariantContext { |
||||
|
size: ButtonProps["size"] |
||||
|
variantMap: ButtonVariantMap |
||||
|
getHref?: (page: number) => string |
||||
|
} |
||||
|
|
||||
|
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({ |
||||
|
name: "RootPropsProvider", |
||||
|
}) |
||||
|
|
||||
|
export interface PaginationRootProps |
||||
|
extends Omit<ChakraPagination.RootProps, "type"> { |
||||
|
size?: ButtonProps["size"] |
||||
|
variant?: PaginationVariant |
||||
|
getHref?: (page: number) => string |
||||
|
} |
||||
|
|
||||
|
const variantMap: Record<PaginationVariant, ButtonVariantMap> = { |
||||
|
outline: { default: "ghost", ellipsis: "plain", current: "outline" }, |
||||
|
solid: { default: "outline", ellipsis: "outline", current: "solid" }, |
||||
|
subtle: { default: "ghost", ellipsis: "plain", current: "subtle" }, |
||||
|
} |
||||
|
|
||||
|
export const PaginationRoot = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
PaginationRootProps |
||||
|
>(function PaginationRoot(props, ref) { |
||||
|
const { size = "sm", variant = "outline", getHref, ...rest } = props |
||||
|
return ( |
||||
|
<RootPropsProvider |
||||
|
value={{ size, variantMap: variantMap[variant], getHref }} |
||||
|
> |
||||
|
<ChakraPagination.Root |
||||
|
ref={ref} |
||||
|
type={getHref ? "link" : "button"} |
||||
|
{...rest} |
||||
|
/> |
||||
|
</RootPropsProvider> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const PaginationEllipsis = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
ChakraPagination.EllipsisProps |
||||
|
>(function PaginationEllipsis(props, ref) { |
||||
|
const { size, variantMap } = useRootProps() |
||||
|
return ( |
||||
|
<ChakraPagination.Ellipsis ref={ref} {...props} asChild> |
||||
|
<Button as="span" variant={variantMap.ellipsis} size={size}> |
||||
|
<HiMiniEllipsisHorizontal /> |
||||
|
</Button> |
||||
|
</ChakraPagination.Ellipsis> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const PaginationItem = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ChakraPagination.ItemProps |
||||
|
>(function PaginationItem(props, ref) { |
||||
|
const { page } = usePaginationContext() |
||||
|
const { size, variantMap, getHref } = useRootProps() |
||||
|
|
||||
|
const current = page === props.value |
||||
|
const variant = current ? variantMap.current : variantMap.default |
||||
|
|
||||
|
if (getHref) { |
||||
|
return ( |
||||
|
<LinkButton href={getHref(props.value)} variant={variant} size={size}> |
||||
|
{props.value} |
||||
|
</LinkButton> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ChakraPagination.Item ref={ref} {...props} asChild> |
||||
|
<Button variant={variant} size={size}> |
||||
|
{props.value} |
||||
|
</Button> |
||||
|
</ChakraPagination.Item> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const PaginationPrevTrigger = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ChakraPagination.PrevTriggerProps |
||||
|
>(function PaginationPrevTrigger(props, ref) { |
||||
|
const { size, variantMap, getHref } = useRootProps() |
||||
|
const { previousPage } = usePaginationContext() |
||||
|
|
||||
|
if (getHref) { |
||||
|
return ( |
||||
|
<LinkButton |
||||
|
href={previousPage != null ? getHref(previousPage) : undefined} |
||||
|
variant={variantMap.default} |
||||
|
size={size} |
||||
|
> |
||||
|
<HiChevronLeft /> |
||||
|
</LinkButton> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ChakraPagination.PrevTrigger ref={ref} asChild {...props}> |
||||
|
<IconButton variant={variantMap.default} size={size}> |
||||
|
<HiChevronLeft /> |
||||
|
</IconButton> |
||||
|
</ChakraPagination.PrevTrigger> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const PaginationNextTrigger = React.forwardRef< |
||||
|
HTMLButtonElement, |
||||
|
ChakraPagination.NextTriggerProps |
||||
|
>(function PaginationNextTrigger(props, ref) { |
||||
|
const { size, variantMap, getHref } = useRootProps() |
||||
|
const { nextPage } = usePaginationContext() |
||||
|
|
||||
|
if (getHref) { |
||||
|
return ( |
||||
|
<LinkButton |
||||
|
href={nextPage != null ? getHref(nextPage) : undefined} |
||||
|
variant={variantMap.default} |
||||
|
size={size} |
||||
|
> |
||||
|
<HiChevronRight /> |
||||
|
</LinkButton> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ChakraPagination.NextTrigger ref={ref} asChild {...props}> |
||||
|
<IconButton variant={variantMap.default} size={size}> |
||||
|
<HiChevronRight /> |
||||
|
</IconButton> |
||||
|
</ChakraPagination.NextTrigger> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => { |
||||
|
return ( |
||||
|
<ChakraPagination.Context> |
||||
|
{({ pages }) => |
||||
|
pages.map((page, index) => { |
||||
|
return page.type === "ellipsis" ? ( |
||||
|
<PaginationEllipsis key={index} index={index} {...props} /> |
||||
|
) : ( |
||||
|
<PaginationItem |
||||
|
key={index} |
||||
|
type="page" |
||||
|
value={page.value} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
}) |
||||
|
} |
||||
|
</ChakraPagination.Context> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
interface PageTextProps extends TextProps { |
||||
|
format?: "short" | "compact" | "long" |
||||
|
} |
||||
|
|
||||
|
export const PaginationPageText = React.forwardRef< |
||||
|
HTMLParagraphElement, |
||||
|
PageTextProps |
||||
|
>(function PaginationPageText(props, ref) { |
||||
|
const { format = "compact", ...rest } = props |
||||
|
const { page, totalPages, pageRange, count } = usePaginationContext() |
||||
|
const content = React.useMemo(() => { |
||||
|
if (format === "short") return `${page} / ${totalPages}` |
||||
|
if (format === "compact") return `${page} of ${totalPages}` |
||||
|
return `${pageRange.start + 1} - ${Math.min( |
||||
|
pageRange.end, |
||||
|
count, |
||||
|
)} of ${count}` |
||||
|
}, [format, page, totalPages, pageRange, count]) |
||||
|
|
||||
|
return ( |
||||
|
<Text fontWeight="medium" ref={ref} {...rest}> |
||||
|
{content} |
||||
|
</Text> |
||||
|
) |
||||
|
}) |
@ -0,0 +1,162 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import type { |
||||
|
ButtonProps, |
||||
|
GroupProps, |
||||
|
InputProps, |
||||
|
StackProps, |
||||
|
} from "@chakra-ui/react" |
||||
|
import { |
||||
|
Box, |
||||
|
HStack, |
||||
|
IconButton, |
||||
|
Input, |
||||
|
Stack, |
||||
|
mergeRefs, |
||||
|
useControllableState, |
||||
|
} from "@chakra-ui/react" |
||||
|
import { forwardRef, useRef } from "react" |
||||
|
import { FiEye, FiEyeOff } from "react-icons/fi" |
||||
|
import { Field } from "./field" |
||||
|
import { InputGroup } from "./input-group" |
||||
|
|
||||
|
export interface PasswordVisibilityProps { |
||||
|
defaultVisible?: boolean |
||||
|
visible?: boolean |
||||
|
onVisibleChange?: (visible: boolean) => void |
||||
|
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode } |
||||
|
} |
||||
|
|
||||
|
export interface PasswordInputProps |
||||
|
extends InputProps, |
||||
|
PasswordVisibilityProps { |
||||
|
rootProps?: GroupProps |
||||
|
startElement?: React.ReactNode |
||||
|
type: string |
||||
|
errors: any |
||||
|
} |
||||
|
|
||||
|
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>( |
||||
|
function PasswordInput(props, ref) { |
||||
|
const { |
||||
|
rootProps, |
||||
|
defaultVisible, |
||||
|
visible: visibleProp, |
||||
|
onVisibleChange, |
||||
|
visibilityIcon = { on: <FiEye />, off: <FiEyeOff /> }, |
||||
|
startElement, |
||||
|
type, |
||||
|
errors, |
||||
|
...rest |
||||
|
} = props |
||||
|
|
||||
|
const [visible, setVisible] = useControllableState({ |
||||
|
value: visibleProp, |
||||
|
defaultValue: defaultVisible || false, |
||||
|
onChange: onVisibleChange, |
||||
|
}) |
||||
|
|
||||
|
const inputRef = useRef<HTMLInputElement>(null) |
||||
|
|
||||
|
return ( |
||||
|
<Field |
||||
|
invalid={!!errors[type]} |
||||
|
errorText={errors[type]?.message} |
||||
|
alignSelf="start" |
||||
|
> |
||||
|
<InputGroup |
||||
|
width="100%" |
||||
|
startElement={startElement} |
||||
|
endElement={ |
||||
|
<VisibilityTrigger |
||||
|
disabled={rest.disabled} |
||||
|
onPointerDown={(e) => { |
||||
|
if (rest.disabled) return |
||||
|
if (e.button !== 0) return |
||||
|
e.preventDefault() |
||||
|
setVisible(!visible) |
||||
|
}} |
||||
|
> |
||||
|
{visible ? visibilityIcon.off : visibilityIcon.on} |
||||
|
</VisibilityTrigger> |
||||
|
} |
||||
|
{...rootProps} |
||||
|
> |
||||
|
<Input |
||||
|
{...rest} |
||||
|
ref={mergeRefs(ref, inputRef)} |
||||
|
type={visible ? "text" : "password"} |
||||
|
/> |
||||
|
</InputGroup> |
||||
|
</Field> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
const VisibilityTrigger = forwardRef<HTMLButtonElement, ButtonProps>( |
||||
|
function VisibilityTrigger(props, ref) { |
||||
|
return ( |
||||
|
<IconButton |
||||
|
tabIndex={-1} |
||||
|
ref={ref} |
||||
|
me="-2" |
||||
|
aspectRatio="square" |
||||
|
size="sm" |
||||
|
variant="ghost" |
||||
|
height="calc(100% - {spacing.2})" |
||||
|
aria-label="Toggle password visibility" |
||||
|
color="inherit" |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
interface PasswordStrengthMeterProps extends StackProps { |
||||
|
max?: number |
||||
|
value: number |
||||
|
} |
||||
|
|
||||
|
export const PasswordStrengthMeter = forwardRef< |
||||
|
HTMLDivElement, |
||||
|
PasswordStrengthMeterProps |
||||
|
>(function PasswordStrengthMeter(props, ref) { |
||||
|
const { max = 4, value, ...rest } = props |
||||
|
|
||||
|
const percent = (value / max) * 100 |
||||
|
const { label, colorPalette } = getColorPalette(percent) |
||||
|
|
||||
|
return ( |
||||
|
<Stack align="flex-end" gap="1" ref={ref} {...rest}> |
||||
|
<HStack width="full" ref={ref} {...rest}> |
||||
|
{Array.from({ length: max }).map((_, index) => ( |
||||
|
<Box |
||||
|
key={index} |
||||
|
height="1" |
||||
|
flex="1" |
||||
|
rounded="sm" |
||||
|
data-selected={index < value ? "" : undefined} |
||||
|
layerStyle="fill.subtle" |
||||
|
colorPalette="gray" |
||||
|
_selected={{ |
||||
|
colorPalette, |
||||
|
layerStyle: "fill.solid", |
||||
|
}} |
||||
|
/> |
||||
|
))} |
||||
|
</HStack> |
||||
|
{label && <HStack textStyle="xs">{label}</HStack>} |
||||
|
</Stack> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
function getColorPalette(percent: number) { |
||||
|
switch (true) { |
||||
|
case percent < 33: |
||||
|
return { label: "Low", colorPalette: "red" } |
||||
|
case percent < 66: |
||||
|
return { label: "Medium", colorPalette: "orange" } |
||||
|
default: |
||||
|
return { label: "High", colorPalette: "green" } |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import { ChakraProvider } from "@chakra-ui/react" |
||||
|
import React, { type PropsWithChildren } from "react" |
||||
|
import { system } from "../../theme" |
||||
|
import { ColorModeProvider } from "./color-mode" |
||||
|
import { Toaster } from "./toaster" |
||||
|
|
||||
|
export function CustomProvider(props: PropsWithChildren) { |
||||
|
return ( |
||||
|
<ChakraProvider value={system}> |
||||
|
<ColorModeProvider defaultTheme="light"> |
||||
|
{props.children} |
||||
|
</ColorModeProvider> |
||||
|
<Toaster /> |
||||
|
</ChakraProvider> |
||||
|
) |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
export interface RadioProps extends ChakraRadioGroup.ItemProps { |
||||
|
rootRef?: React.Ref<HTMLDivElement> |
||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement> |
||||
|
} |
||||
|
|
||||
|
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>( |
||||
|
function Radio(props, ref) { |
||||
|
const { children, inputProps, rootRef, ...rest } = props |
||||
|
return ( |
||||
|
<ChakraRadioGroup.Item ref={rootRef} {...rest}> |
||||
|
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} /> |
||||
|
<ChakraRadioGroup.ItemIndicator /> |
||||
|
{children && ( |
||||
|
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText> |
||||
|
)} |
||||
|
</ChakraRadioGroup.Item> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
export const RadioGroup = ChakraRadioGroup.Root |
@ -0,0 +1,47 @@ |
|||||
|
import type { |
||||
|
SkeletonProps as ChakraSkeletonProps, |
||||
|
CircleProps, |
||||
|
} from "@chakra-ui/react" |
||||
|
import { Skeleton as ChakraSkeleton, Circle, Stack } from "@chakra-ui/react" |
||||
|
import * as React from "react" |
||||
|
|
||||
|
export interface SkeletonCircleProps extends ChakraSkeletonProps { |
||||
|
size?: CircleProps["size"] |
||||
|
} |
||||
|
|
||||
|
export const SkeletonCircle = React.forwardRef< |
||||
|
HTMLDivElement, |
||||
|
SkeletonCircleProps |
||||
|
>(function SkeletonCircle(props, ref) { |
||||
|
const { size, ...rest } = props |
||||
|
return ( |
||||
|
<Circle size={size} asChild ref={ref}> |
||||
|
<ChakraSkeleton {...rest} /> |
||||
|
</Circle> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export interface SkeletonTextProps extends ChakraSkeletonProps { |
||||
|
noOfLines?: number |
||||
|
} |
||||
|
|
||||
|
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>( |
||||
|
function SkeletonText(props, ref) { |
||||
|
const { noOfLines = 3, gap, ...rest } = props |
||||
|
return ( |
||||
|
<Stack gap={gap} width="full" ref={ref}> |
||||
|
{Array.from({ length: noOfLines }).map((_, index) => ( |
||||
|
<ChakraSkeleton |
||||
|
height="4" |
||||
|
key={index} |
||||
|
{...props} |
||||
|
_last={{ maxW: "80%" }} |
||||
|
{...rest} |
||||
|
/> |
||||
|
))} |
||||
|
</Stack> |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
export const Skeleton = ChakraSkeleton |
@ -0,0 +1,43 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import { |
||||
|
Toaster as ChakraToaster, |
||||
|
Portal, |
||||
|
Spinner, |
||||
|
Stack, |
||||
|
Toast, |
||||
|
createToaster, |
||||
|
} from "@chakra-ui/react" |
||||
|
|
||||
|
export const toaster = createToaster({ |
||||
|
placement: "top-end", |
||||
|
pauseOnPageIdle: true, |
||||
|
}) |
||||
|
|
||||
|
export const Toaster = () => { |
||||
|
return ( |
||||
|
<Portal> |
||||
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}> |
||||
|
{(toast) => ( |
||||
|
<Toast.Root width={{ md: "sm" }} color={toast.meta?.color}> |
||||
|
{toast.type === "loading" ? ( |
||||
|
<Spinner size="sm" color="blue.solid" /> |
||||
|
) : ( |
||||
|
<Toast.Indicator /> |
||||
|
)} |
||||
|
<Stack gap="1" flex="1" maxWidth="100%"> |
||||
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>} |
||||
|
{toast.description && ( |
||||
|
<Toast.Description>{toast.description}</Toast.Description> |
||||
|
)} |
||||
|
</Stack> |
||||
|
{toast.action && ( |
||||
|
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger> |
||||
|
)} |
||||
|
{toast.meta?.closable && <Toast.CloseTrigger />} |
||||
|
</Toast.Root> |
||||
|
)} |
||||
|
</ChakraToaster> |
||||
|
</Portal> |
||||
|
) |
||||
|
} |
@ -1,23 +1,25 @@ |
|||||
import { useToast } from "@chakra-ui/react" |
"use client" |
||||
import { useCallback } from "react" |
|
||||
|
import { toaster } from "../components/ui/toaster" |
||||
|
|
||||
const useCustomToast = () => { |
const useCustomToast = () => { |
||||
const toast = useToast() |
const showSuccessToast = (description: string) => { |
||||
|
toaster.create({ |
||||
|
title: "Success!", |
||||
|
description, |
||||
|
type: "success", |
||||
|
}) |
||||
|
} |
||||
|
|
||||
const showToast = useCallback( |
const showErrorToast = (description: string) => { |
||||
(title: string, description: string, status: "success" | "error") => { |
toaster.create({ |
||||
toast({ |
title: "Something went wrong!", |
||||
title, |
description, |
||||
description, |
type: "error", |
||||
status, |
}) |
||||
isClosable: true, |
} |
||||
position: "bottom-right", |
|
||||
}) |
|
||||
}, |
|
||||
[toast], |
|
||||
) |
|
||||
|
|
||||
return showToast |
return { showSuccessToast, showErrorToast } |
||||
} |
} |
||||
|
|
||||
export default useCustomToast |
export default useCustomToast |
||||
|
@ -0,0 +1,21 @@ |
|||||
|
import { defineRecipe } from "@chakra-ui/react" |
||||
|
|
||||
|
export const buttonRecipe = defineRecipe({ |
||||
|
base: { |
||||
|
fontWeight: "bold", |
||||
|
display: "flex", |
||||
|
alignItems: "center", |
||||
|
justifyContent: "center", |
||||
|
colorPalette: "teal", |
||||
|
}, |
||||
|
variants: { |
||||
|
variant: { |
||||
|
ghost: { |
||||
|
bg: "transparent", |
||||
|
_hover: { |
||||
|
bg: "gray.100", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
Loading…
Reference in new issue