refactor(web/menu): list menu mantine rewrite

This commit is contained in:
Luke
2023-01-02 13:45:19 +01:00
parent d5cdfe58f2
commit d48b5e199b
4 changed files with 179 additions and 107 deletions

View File

@@ -1,35 +1,29 @@
import { Box, Flex, useCheckbox, chakra, CheckboxIcon } from '@chakra-ui/react'; import { Checkbox, createStyles, Stack } from '@mantine/core';
const useStyles = createStyles((theme) => ({
root: {
display: 'flex',
alignItems: 'center',
},
input: {
backgroundColor: theme.colors.dark[7],
'&:checked': { backgroundColor: theme.colors.dark[2], borderColor: theme.colors.dark[2] },
},
inner: {
'> svg > path': {
fill: theme.colors.dark[6],
},
},
}));
const CustomCheckbox: React.FC<{ checked: boolean }> = ({ checked }) => { const CustomCheckbox: React.FC<{ checked: boolean }> = ({ checked }) => {
const { getCheckboxProps, getInputProps, htmlProps } = useCheckbox(); const { classes } = useStyles();
return ( return (
<chakra.label <Checkbox
display="flex" checked={checked}
flexDirection="row" size="md"
alignItems="center" classNames={{ root: classes.root, input: classes.input, inner: classes.inner }}
gridColumnGap={2} />
pr={3}
cursor="pointer"
{...htmlProps}
>
<input {...getInputProps()} hidden />
<Flex
alignItems="center"
justifyContent="center"
border="2px solid"
borderColor="#909296"
rounded={'sm'}
w={5}
h={5}
{...getCheckboxProps()}
>
{checked && (
<Box w={4} h={4} bg="#909296">
<CheckboxIcon isChecked color="#25262B" />
</Box>
)}
</Flex>
</chakra.label>
); );
}; };

View File

@@ -1,20 +1,31 @@
import { Box, Text } from '@chakra-ui/react'; import { Box, createStyles, Text } from '@mantine/core';
import React from 'react'; import React from 'react';
const useStyles = createStyles((theme) => ({
container: {
textAlign: 'center',
borderTopLeftRadius: theme.radius.md,
borderTopRightRadius: theme.radius.md,
backgroundColor: theme.colors.dark[6],
height: 60,
width: 384,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
heading: {
fontSize: 24,
textTransform: 'uppercase',
fontWeight: 500,
},
}));
const Header: React.FC<{ title: string }> = ({ title }) => { const Header: React.FC<{ title: string }> = ({ title }) => {
const { classes } = useStyles();
return ( return (
<Box <Box className={classes.container}>
p={3} <Text className={classes.heading}>{title}</Text>
textAlign="center"
borderTopLeftRadius="md"
borderTopRightRadius="md"
bg="#25262B"
height="60px"
width="sm"
>
<Text fontSize={24} textTransform="uppercase" fontWeight={600} fontFamily="Nunito">
{title}
</Text>
</Box> </Box>
); );
}; };

View File

@@ -1,8 +1,9 @@
import { Box, Flex, Stack, Text, Progress } from '@chakra-ui/react'; import { Box, Group, Stack, Text, Progress } from '@mantine/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import CustomCheckbox from './CustomCheckbox'; import CustomCheckbox from './CustomCheckbox';
import type { MenuItem } from './index'; import type { MenuItem } from './index';
import { createStyles } from '@mantine/core';
interface Props { interface Props {
item: MenuItem; item: MenuItem;
@@ -11,35 +12,80 @@ interface Props {
checked: boolean; checked: boolean;
} }
const useStyles = createStyles((theme, params: { iconColor?: string }) => ({
buttonContainer: {
backgroundColor: theme.colors.dark[6],
borderRadius: theme.radius.md,
padding: 2,
height: 60,
scrollMargin: 8,
'&:focus': {
backgroundColor: theme.colors.dark[4],
outline: 'none',
},
},
buttonWrapper: {
paddingLeft: 5,
paddingRight: 12,
height: '100%',
},
iconContainer: {
display: 'flex',
alignItems: 'center',
},
icon: {
fontSize: 24,
color: params.iconColor || theme.colors.dark[2],
},
label: {
color: theme.colors.dark[2],
textTransform: 'uppercase',
fontSize: 12,
verticalAlign: 'middle',
},
chevronIcon: {
fontSize: 14,
color: theme.colors.dark[2],
},
scrollIndexValue: {
color: theme.colors.dark[2],
textTransform: 'uppercase',
fontSize: 14,
},
progressStack: {
width: '100%',
marginRight: 5,
},
progressLabel: {
verticalAlign: 'middle',
marginBottom: 3,
},
}));
const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex, checked }, ref) => { const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex, checked }, ref) => {
const { classes } = useStyles({ iconColor: item.iconColor });
return ( return (
<Box <Box
bg="#25262B"
borderRadius="md"
tabIndex={index} tabIndex={index}
scrollMargin={2} className={classes.buttonContainer}
p={2}
height="60px"
key={`item-${index}`} key={`item-${index}`}
_focus={{ bg: '#373A40', outline: 'none' }} ref={(element: HTMLDivElement) => {
ref={(element) => {
if (ref) if (ref)
// @ts-ignore i cba // @ts-ignore i cba
return (ref.current = [...ref.current, element]); return (ref.current = [...ref.current, element]);
}} }}
> >
<Flex alignItems="center" height="100%" gap="15px"> <Group spacing={15} noWrap className={classes.buttonWrapper}>
{item.icon && ( {item.icon && (
<Box display="flex" alignItems="center"> <Box className={classes.iconContainer}>
<FontAwesomeIcon icon={item.icon} fontSize={24} color={item.iconColor || '#909296'} fixedWidth /> <FontAwesomeIcon icon={item.icon} className={classes.icon} fixedWidth />
</Box> </Box>
)} )}
{Array.isArray(item.values) ? ( {Array.isArray(item.values) ? (
<Flex alignItems="center" justifyContent="space-between" w="100%"> <Group position="apart" w="100%">
<Stack spacing={1} justifyContent="space-between"> <Stack spacing={0} justify="space-between">
<Text color="#909296" textTransform="uppercase" fontSize={12} verticalAlign="middle"> <Text className={classes.label}>{item.label}</Text>
{item.label}
</Text>
<Text> <Text>
{typeof item.values[scrollIndex] === 'object' {typeof item.values[scrollIndex] === 'object'
? // @ts-ignore for some reason even checking the type TS still thinks it's a string ? // @ts-ignore for some reason even checking the type TS still thinks it's a string
@@ -47,30 +93,32 @@ const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index,
: item.values[scrollIndex]} : item.values[scrollIndex]}
</Text> </Text>
</Stack> </Stack>
<Stack direction="row" spacing="sm" pr={3} justifyContent="center" alignItems="center"> <Group spacing={1} position="center">
<FontAwesomeIcon icon="chevron-left" fontSize={16} color="#909296" /> <FontAwesomeIcon icon="chevron-left" className={classes.chevronIcon} />
<Text color="#909296" textTransform="uppercase" fontSize={14}> <Text className={classes.scrollIndexValue}>
{scrollIndex + 1}/{item.values.length} {scrollIndex + 1}/{item.values.length}
</Text> </Text>
<FontAwesomeIcon icon="chevron-right" fontSize={16} color="#909296" /> <FontAwesomeIcon icon="chevron-right" className={classes.chevronIcon} />
</Stack> </Group>
</Flex> </Group>
) : item.checked !== undefined ? ( ) : item.checked !== undefined ? (
<Flex alignItems="center" justifyContent="space-between" w="100%"> <Group position="apart" w="100%">
<Text>{item.label}</Text> <Text>{item.label}</Text>
<CustomCheckbox checked={checked}></CustomCheckbox> <CustomCheckbox checked={checked}></CustomCheckbox>
</Flex> </Group>
) : item.progress !== undefined ? ( ) : item.progress !== undefined ? (
<Flex flexDirection="column" w="100%" marginRight="5px"> <Stack className={classes.progressStack} spacing={0}>
<Text verticalAlign="middle" marginBottom="3px"> <Text className={classes.progressLabel}>{item.label}</Text>
{item.label} <Progress
</Text> value={item.progress}
<Progress value={item.progress} size="sm" colorScheme={item.colorScheme || 'gray'} borderRadius="md" /> color={item.colorScheme || 'dark.0'}
</Flex> styles={(theme) => ({ root: { backgroundColor: theme.colors.dark[3] } })}
/>
</Stack>
) : ( ) : (
<Text>{item.label}</Text> <Text>{item.label}</Text>
)} )}
</Flex> </Group>
</Box> </Box>
); );
}); });

View File

@@ -1,4 +1,4 @@
import { Box, Stack, Tooltip } from '@chakra-ui/react'; import { Box, createStyles, Stack, Tooltip } from '@mantine/core';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useNuiEvent } from '../../../hooks/useNuiEvent'; import { useNuiEvent } from '../../../hooks/useNuiEvent';
import ListItem from './ListItem'; import ListItem from './ListItem';
@@ -9,6 +9,8 @@ import { fetchNui } from '../../../utils/fetchNui';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react'; import React from 'react';
type MenuPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export interface MenuItem { export interface MenuItem {
label: string; label: string;
progress?: number; progress?: number;
@@ -23,13 +25,53 @@ export interface MenuItem {
} }
export interface MenuSettings { export interface MenuSettings {
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; position?: MenuPosition;
title: string; title: string;
canClose?: boolean; canClose?: boolean;
items: Array<MenuItem>; items: Array<MenuItem>;
startItemIndex?: number; startItemIndex?: number;
} }
const useStyles = createStyles((theme, params: { position?: MenuPosition; itemCount: number; selected: number }) => ({
tooltip: {
backgroundColor: theme.colors.dark[6],
color: theme.colors.dark[2],
borderRadius: theme.radius.sm,
},
container: {
position: 'absolute',
pointerEvents: 'none',
marginTop: params.position === 'top-left' || params.position === 'top-right' ? 5 : 0,
marginLeft: params.position === 'top-left' || params.position === 'bottom-left' ? 5 : 0,
marginRight: params.position === 'top-right' || params.position === 'bottom-right' ? 5 : 0,
marginBottom: params.position === 'bottom-left' || params.position === 'bottom-right' ? 5 : 0,
right: params.position === 'top-right' || params.position === 'bottom-right' ? 1 : undefined,
left: params.position === 'bottom-left' ? 1 : undefined,
bottom: params.position === 'bottom-left' || params.position === 'bottom-right' ? 1 : undefined,
fontFamily: 'Roboto',
},
buttonsWrapper: {
height: 'fit-content',
maxHeight: 415,
overflow: 'hidden',
borderRadius: params.itemCount <= 6 || params.selected === params.itemCount - 1 ? theme.radius.md : undefined,
backgroundColor: theme.colors.dark[8],
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
scrollArrow: {
backgroundColor: theme.colors.dark[8],
textAlign: 'center',
borderBottomLeftRadius: theme.radius.md,
borderBottomRightRadius: theme.radius.md,
height: 25,
},
scrollArrowIcon: {
color: theme.colors.dark[2],
fontSize: 20,
},
}));
const ListMenu: React.FC = () => { const ListMenu: React.FC = () => {
const [menu, setMenu] = useState<MenuSettings>({ const [menu, setMenu] = useState<MenuSettings>({
position: 'top-left', position: 'top-left',
@@ -42,6 +84,7 @@ const ListMenu: React.FC = () => {
const [checkedStates, setCheckedStates] = useState<Record<number, boolean>>({}); const [checkedStates, setCheckedStates] = useState<Record<number, boolean>>({});
const listRefs = useRef<Array<HTMLDivElement | null>>([]); const listRefs = useRef<Array<HTMLDivElement | null>>([]);
const firstRenderRef = useRef(false); const firstRenderRef = useRef(false);
const { classes } = useStyles({ position: menu.position, itemCount: menu.items.length, selected });
const closeMenu = (ignoreFetch?: boolean, keyPressed?: string, forceClose?: boolean) => { const closeMenu = (ignoreFetch?: boolean, keyPressed?: string, forceClose?: boolean) => {
if (menu.canClose === false && !forceClose) return; if (menu.canClose === false && !forceClose) return;
@@ -184,44 +227,20 @@ const ListMenu: React.FC = () => {
menu.items[selected].values[indexStates[selected]].description menu.items[selected].values[indexStates[selected]].description
: menu.items[selected].description : menu.items[selected].description
} }
isOpen={ opened={
isValuesObject(menu.items[selected].values) isValuesObject(menu.items[selected].values)
? // @ts-ignore ? // @ts-ignore
!!menu.items[selected].values[indexStates[selected]].description !!menu.items[selected].values[indexStates[selected]].description
: !!menu.items[selected].description : !!menu.items[selected].description
} }
bg="#25262B" transitionDuration={0}
color="#909296" classNames={{ tooltip: classes.tooltip }}
placement="bottom"
borderRadius="md"
fontFamily="Nunito"
>
<Box
position="absolute"
pointerEvents="none"
mt={menu.position === 'top-left' || menu.position === 'top-right' ? 5 : 0}
ml={menu.position === 'top-left' || menu.position === 'bottom-left' ? 5 : 0}
mr={menu.position === 'top-right' || menu.position === 'bottom-right' ? 5 : 0}
mb={menu.position === 'bottom-left' || menu.position === 'bottom-right' ? 5 : 0}
right={menu.position === 'top-right' || menu.position === 'bottom-right' ? 1 : undefined}
left={menu.position === 'bottom-left' ? 1 : undefined}
bottom={menu.position === 'bottom-left' || menu.position === 'bottom-right' ? 1 : undefined}
> >
<Box className={classes.container}>
<Header title={menu.title} /> <Header title={menu.title} />
<Box <Box className={classes.buttonsWrapper} onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => moveMenu(e)}>
width="sm"
height="fit-content"
maxHeight={415}
overflow="hidden"
borderRadius={menu.items.length <= 6 || selected === menu.items.length - 1 ? 'md' : undefined}
bg="#141517"
fontFamily="Nunito"
borderTopLeftRadius="none"
borderTopRightRadius="none"
onKeyDown={(e) => moveMenu(e)}
>
<FocusTrap active={visible}> <FocusTrap active={visible}>
<Stack direction="column" p={2} overflowY="scroll"> <Stack spacing={8} p={8} sx={{ overflowY: 'scroll' }}>
{menu.items.map((item, index) => ( {menu.items.map((item, index) => (
<React.Fragment key={`menu-item-${index}`}> <React.Fragment key={`menu-item-${index}`}>
{item.label && ( {item.label && (
@@ -239,8 +258,8 @@ const ListMenu: React.FC = () => {
</FocusTrap> </FocusTrap>
</Box> </Box>
{menu.items.length > 6 && selected !== menu.items.length - 1 && ( {menu.items.length > 6 && selected !== menu.items.length - 1 && (
<Box bg="#141517" textAlign="center" borderBottomLeftRadius="md" borderBottomRightRadius="md" height={25}> <Box className={classes.scrollArrow}>
<FontAwesomeIcon icon="chevron-down" color="#909296" fontSize={20} /> <FontAwesomeIcon icon="chevron-down" className={classes.scrollArrowIcon} />
</Box> </Box>
)} )}
</Box> </Box>