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 { |
|||
Button, |
|||
Container, |
|||
Heading, |
|||
Text, |
|||
useDisclosure, |
|||
} from "@chakra-ui/react" |
|||
import { Container, Heading, Text } from "@chakra-ui/react" |
|||
|
|||
import DeleteConfirmation from "./DeleteConfirmation" |
|||
|
|||
const DeleteAccount = () => { |
|||
const confirmationModal = useDisclosure() |
|||
|
|||
return ( |
|||
<> |
|||
<Container maxW="full"> |
|||
<Heading size="sm" py={4}> |
|||
Delete Account |
|||
</Heading> |
|||
<Text> |
|||
Permanently delete your data and everything associated with your |
|||
account. |
|||
</Text> |
|||
<Button variant="danger" mt={4} onClick={confirmationModal.onOpen}> |
|||
Delete |
|||
</Button> |
|||
<DeleteConfirmation |
|||
isOpen={confirmationModal.isOpen} |
|||
onClose={confirmationModal.onClose} |
|||
/> |
|||
</Container> |
|||
</> |
|||
<Container maxW="full"> |
|||
<Heading size="sm" py={4}> |
|||
Delete Account |
|||
</Heading> |
|||
<Text> |
|||
Permanently delete your data and everything associated with your |
|||
account. |
|||
</Text> |
|||
<DeleteConfirmation /> |
|||
</Container> |
|||
) |
|||
} |
|||
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" |
|||
import { useCallback } from "react" |
|||
"use client" |
|||
|
|||
import { toaster } from "../components/ui/toaster" |
|||
|
|||
const useCustomToast = () => { |
|||
const toast = useToast() |
|||
const showSuccessToast = (description: string) => { |
|||
toaster.create({ |
|||
title: "Success!", |
|||
description, |
|||
type: "success", |
|||
}) |
|||
} |
|||
|
|||
const showToast = useCallback( |
|||
(title: string, description: string, status: "success" | "error") => { |
|||
toast({ |
|||
title, |
|||
description, |
|||
status, |
|||
isClosable: true, |
|||
position: "bottom-right", |
|||
}) |
|||
}, |
|||
[toast], |
|||
) |
|||
const showErrorToast = (description: string) => { |
|||
toaster.create({ |
|||
title: "Something went wrong!", |
|||
description, |
|||
type: "error", |
|||
}) |
|||
} |
|||
|
|||
return showToast |
|||
return { showSuccessToast, showErrorToast } |
|||
} |
|||
|
|||
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