mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-17 06:56:03 +01:00
refactor(web/context): context menu mantine rewrite
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
||||||
import { Box, Text, Flex, ScaleFade } from '@chakra-ui/react';
|
import { Box, Stack, Text, Flex, Transition } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ContextMenuProps } from '../../../interfaces/context';
|
import { ContextMenuProps } from '../../../interfaces/context';
|
||||||
import Item from './Item';
|
import ContextButton from './components/ContextButton';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
import { fetchNui } from '../../../utils/fetchNui';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import HeaderButton from './components/HeaderButton';
|
||||||
|
|
||||||
const openMenu = (id: string | undefined) => {
|
const openMenu = (id: string | undefined) => {
|
||||||
fetchNui<ContextMenuProps>('openContext', { id: id, back: true });
|
fetchNui<ContextMenuProps>('openContext', { id: id, back: true });
|
||||||
@@ -49,64 +49,30 @@ const ContextMenu: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="absolute" w="75%" h="80%" justifyContent="flex-end" alignItems="center">
|
<Transition mounted={visible} transition="pop" duration={100} exitDuration={100}>
|
||||||
<ScaleFade in={visible} unmountOnExit>
|
{(styles) => (
|
||||||
<Box w="xs" h={580}>
|
<Box style={styles} sx={{ position: 'absolute', top: '15%', right: '25%' }} w={320} h={580}>
|
||||||
<Flex justifyContent="center" alignItems="center" mb={3}>
|
<Flex justify="center" align="center" mb={10} gap={6}>
|
||||||
{contextMenu.menu && (
|
{contextMenu.menu && (
|
||||||
<Flex
|
<HeaderButton icon="chevron-left" iconSize={16} handleClick={() => openMenu(contextMenu.menu)} />
|
||||||
borderRadius="md"
|
|
||||||
bg="gray.800"
|
|
||||||
flex="1 15%"
|
|
||||||
alignSelf="stretch"
|
|
||||||
textAlign="center"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
marginRight={2}
|
|
||||||
p={2}
|
|
||||||
_hover={{ bg: 'gray.700' }}
|
|
||||||
transition="300ms"
|
|
||||||
onClick={() => openMenu(contextMenu.menu)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon="chevron-left" />
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
<Box borderRadius="md" bg="gray.800" flex="1 85%">
|
<Box sx={{ borderRadius: 4, flex: '1 85%' }} bg="dark.6">
|
||||||
<Text fontFamily="Poppins" fontSize="md" p={2} textAlign="center" fontWeight="light">
|
<Text color="dark.0" p={6} align="center">
|
||||||
<ReactMarkdown>{contextMenu.title}</ReactMarkdown>
|
<ReactMarkdown>{contextMenu.title}</ReactMarkdown>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex
|
<HeaderButton icon="xmark" canClose={contextMenu.canClose} iconSize={18} handleClick={closeContext} />
|
||||||
borderRadius="md"
|
|
||||||
as="button"
|
|
||||||
bg={contextMenu.canClose === false ? 'gray.600' : 'gray.800'}
|
|
||||||
flex="1 15%"
|
|
||||||
alignSelf="stretch"
|
|
||||||
textAlign="center"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
marginLeft={2}
|
|
||||||
p={2}
|
|
||||||
cursor={contextMenu.canClose === false ? 'not-allowed' : undefined}
|
|
||||||
_hover={{ bg: contextMenu.canClose === false ? undefined : 'gray.700' }}
|
|
||||||
transition="300ms"
|
|
||||||
onClick={() => closeContext()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="xmark"
|
|
||||||
fontSize={20}
|
|
||||||
color={contextMenu.canClose === false ? '#718096' : undefined}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box maxH={560} overflowY="scroll">
|
<Box sx={{ height: 560, overflowY: 'scroll' }}>
|
||||||
{Object.entries(contextMenu.options).map((option, index) => (
|
<Stack spacing={3}>
|
||||||
<Item option={option} key={`context-item-${index}`} />
|
{Object.entries(contextMenu.options).map((option, index) => (
|
||||||
))}
|
<ContextButton option={option} key={`context-item-${index}`} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ScaleFade>
|
)}
|
||||||
</Flex>
|
</Transition>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import {
|
|
||||||
Portal,
|
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverBody,
|
|
||||||
PopoverContent,
|
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Flex,
|
|
||||||
Spacer,
|
|
||||||
Image,
|
|
||||||
HStack,
|
|
||||||
Progress,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import { Option, ContextMenuProps } from '../../../interfaces/context';
|
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
|
||||||
|
|
||||||
const openMenu = (id: string | undefined) => {
|
|
||||||
fetchNui<ContextMenuProps>('openContext', { id: id, back: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContext = (id: string) => {
|
|
||||||
fetchNui('clickContext', id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item: React.FC<{
|
|
||||||
option: [string, Option];
|
|
||||||
}> = ({ option }) => {
|
|
||||||
const button = option[1];
|
|
||||||
const buttonKey = option[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Popover placement="right-start" trigger="hover" eventListeners={{ scroll: true }} isLazy>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<Box
|
|
||||||
bg={button.disabled ? 'gray.600' : 'gray.800'}
|
|
||||||
borderRadius="md"
|
|
||||||
h="fit-content"
|
|
||||||
w="100%"
|
|
||||||
p={2}
|
|
||||||
mb={1}
|
|
||||||
fontFamily="Poppins"
|
|
||||||
fontSize="md"
|
|
||||||
transition="300ms"
|
|
||||||
_hover={{ bg: !button.disabled && 'gray.700' }}
|
|
||||||
cursor={(button.disabled && 'not-allowed') || undefined}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
w="100%"
|
|
||||||
alignItems="center"
|
|
||||||
color={button.disabled ? '#718096' : undefined}
|
|
||||||
onClick={() =>
|
|
||||||
!button.disabled ? (button.menu ? openMenu(button.menu) : clickContext(buttonKey)) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{button?.icon && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
fixedWidth
|
|
||||||
icon={button.icon}
|
|
||||||
fontSize={20}
|
|
||||||
style={{
|
|
||||||
marginRight: 10,
|
|
||||||
justifySelf: 'center',
|
|
||||||
color: button.iconColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box w="100%">
|
|
||||||
<Box>
|
|
||||||
<Text w="100%" fontWeight="medium" color={button.disabled ? '#718096' : undefined}>
|
|
||||||
<ReactMarkdown>{button.title ? button.title : buttonKey}</ReactMarkdown>
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{button.description && (
|
|
||||||
<Box paddingBottom={1} color={button.disabled ? '#718096' : undefined}>
|
|
||||||
<Text><ReactMarkdown>{button.description}</ReactMarkdown></Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{button?.progress && (
|
|
||||||
<Progress
|
|
||||||
value={button.progress}
|
|
||||||
size="sm"
|
|
||||||
colorScheme={button.colorScheme || 'gray'}
|
|
||||||
borderRadius="md"
|
|
||||||
marginRight="5px"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{(button.menu || button.arrow) && button.arrow !== false && (
|
|
||||||
<>
|
|
||||||
<Spacer />
|
|
||||||
<Box alignSelf="center" justifySelf="center" mr={4} fontSize="xl">
|
|
||||||
<FontAwesomeIcon icon="chevron-right" />
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Portal>
|
|
||||||
{!button.disabled && (button.metadata || button.image) && (
|
|
||||||
<PopoverContent
|
|
||||||
fontFamily="Poppins"
|
|
||||||
bg="gray.800"
|
|
||||||
outline="none"
|
|
||||||
border="none"
|
|
||||||
w="fit-content"
|
|
||||||
maxW="2xs"
|
|
||||||
>
|
|
||||||
<PopoverBody>
|
|
||||||
<>
|
|
||||||
{button.image && <Image src={button.image} />}
|
|
||||||
{Array.isArray(button.metadata) ? (
|
|
||||||
button.metadata.map((metadata: string | { label: string; value: any }, index: number) => (
|
|
||||||
<Text key={`context-metadata-${index}`}>
|
|
||||||
{typeof metadata === 'string' ? `${metadata}` : `${metadata.label}: ${metadata.value}`}
|
|
||||||
</Text>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{typeof button.metadata === 'object' &&
|
|
||||||
Object.entries(button.metadata).map((metadata: { [key: string]: any }, index) => (
|
|
||||||
<Text key={`context-metadata-${index}`}>
|
|
||||||
{metadata[0]}: {metadata[1]}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
)}
|
|
||||||
</Portal>
|
|
||||||
</Box>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Item;
|
|
||||||
110
web/src/features/menu/context/components/ContextButton.tsx
Normal file
110
web/src/features/menu/context/components/ContextButton.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Button, Box, Group, Stack, Text, Progress, HoverCard, Image, createStyles } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Option, ContextMenuProps } from '../../../../interfaces/context';
|
||||||
|
import { fetchNui } from '../../../../utils/fetchNui';
|
||||||
|
|
||||||
|
const openMenu = (id: string | undefined) => {
|
||||||
|
fetchNui<ContextMenuProps>('openContext', { id: id, back: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickContext = (id: string) => {
|
||||||
|
fetchNui('clickContext', id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, params: { disabled?: boolean }) => ({
|
||||||
|
inner: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
width: '100%',
|
||||||
|
color: params.disabled ? theme.colors.dark[3] : theme.colors.dark[0],
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
padding: 10,
|
||||||
|
color: theme.colors.dark[0],
|
||||||
|
fontSize: 14,
|
||||||
|
maxWidth: 256,
|
||||||
|
width: 'fit-content',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ContextButton: React.FC<{
|
||||||
|
option: [string, Option];
|
||||||
|
}> = ({ option }) => {
|
||||||
|
const button = option[1];
|
||||||
|
const buttonKey = option[0];
|
||||||
|
const { classes } = useStyles({ disabled: button.disabled });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HoverCard
|
||||||
|
position="right-start"
|
||||||
|
disabled={button.disabled || !(button.metadata || button.image)}
|
||||||
|
openDelay={200}
|
||||||
|
>
|
||||||
|
<HoverCard.Target>
|
||||||
|
<Button
|
||||||
|
classNames={{ inner: classes.inner, label: classes.label }}
|
||||||
|
onClick={() => (!button.disabled ? (button.menu ? openMenu(button.menu) : clickContext(buttonKey)) : null)}
|
||||||
|
variant="default"
|
||||||
|
h="fit-content"
|
||||||
|
p={10}
|
||||||
|
fullWidth
|
||||||
|
disabled={button.disabled}
|
||||||
|
>
|
||||||
|
<Group position="apart" w="100%" noWrap>
|
||||||
|
<Stack spacing={4} style={{ flex: '1' }}>
|
||||||
|
<Group spacing={8} noWrap>
|
||||||
|
{button?.icon && (
|
||||||
|
<Stack w={25} h={25} justify="center" align="center">
|
||||||
|
<FontAwesomeIcon icon={button.icon} fixedWidth size="lg" style={{ color: button.iconColor }} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Text sx={{ overflowWrap: 'break-word' }}>
|
||||||
|
<ReactMarkdown>{button.title || buttonKey}</ReactMarkdown>
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{button.description && (
|
||||||
|
<Text size={12}>
|
||||||
|
<ReactMarkdown>{button.description}</ReactMarkdown>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{button?.progress && (
|
||||||
|
<Progress value={button.progress} size="sm" color={button.colorScheme || 'dark.3'} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{(button.menu || button.arrow) && button.arrow !== false && (
|
||||||
|
<Stack justify="center" w={25} h={25} align="center">
|
||||||
|
<FontAwesomeIcon icon="chevron-right" fixedWidth />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown className={classes.dropdown}>
|
||||||
|
{button.image && <Image src={button.image} />}
|
||||||
|
{Array.isArray(button.metadata) ? (
|
||||||
|
button.metadata.map((metadata: string | { label: string; value: any }, index: number) => (
|
||||||
|
<Text key={`context-metadata-${index}`}>
|
||||||
|
{typeof metadata === 'string' ? `${metadata}` : `${metadata.label}: ${metadata.value}`}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{typeof button.metadata === 'object' &&
|
||||||
|
Object.entries(button.metadata).map((metadata: { [key: string]: any }, index) => (
|
||||||
|
<Text key={`context-metadata-${index}`}>
|
||||||
|
{metadata[0]}: {metadata[1]}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextButton;
|
||||||
44
web/src/features/menu/context/components/HeaderButton.tsx
Normal file
44
web/src/features/menu/context/components/HeaderButton.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Button, createStyles } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: IconProp;
|
||||||
|
canClose?: boolean;
|
||||||
|
iconSize: number;
|
||||||
|
handleClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, params: { canClose?: boolean }) => ({
|
||||||
|
button: {
|
||||||
|
borderRadius: 4,
|
||||||
|
flex: '1 15%',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
height: 'auto',
|
||||||
|
textAlign: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
color: params.canClose ? theme.colors.dark[0] : theme.colors.dark[2],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const HeaderButton: React.FC<Props> = ({ icon, canClose, iconSize, handleClick }) => {
|
||||||
|
const { classes } = useStyles({ canClose });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className={classes.button}
|
||||||
|
classNames={{ label: classes.label }}
|
||||||
|
disabled={canClose === false}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} fontSize={iconSize} fixedWidth />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderButton;
|
||||||
Reference in New Issue
Block a user