mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-18 23:46:03 +01:00
refactor(web/menu): list menu mantine rewrite
This commit is contained in:
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
<Box className={classes.container}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user