feat(web/menu): initial list menu design

Highly subject to change
This commit is contained in:
Luke
2022-07-26 15:35:22 +02:00
committed by Luke
parent e1bc9a5015
commit 303d81c196
6 changed files with 136 additions and 15 deletions

View File

@@ -0,0 +1,152 @@
import { useNuiEvent } from "../../../hooks/useNuiEvent";
import { Box, Text, Flex, ScaleFade } from "@chakra-ui/react";
import { debugData } from "../../../utils/debugData";
import { useEffect, useState } from "react";
import { ContextMenuProps } from "../../../interfaces/context";
import Item from "./Item";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { fetchNui } from "../../../utils/fetchNui";
debugData<ContextMenuProps>([
{
action: "showContext",
data: {
title: "Vehicle garage",
options: [
{ title: "Empty button" },
{
title: "Example button",
description: "Example button description",
icon: "inbox",
image: "https://i.imgur.com/YAe7k17.jpeg",
metadata: [{ label: "Value 1", value: 300 }],
},
{
title: "Menu button",
icon: "bars",
menu: "other_example_menu",
description: "Takes you to another menu",
metadata: ["It also has metadata support"],
},
{
title: "Event button",
description: "Open a menu and send event data",
icon: "check",
arrow: true,
event: "some_event",
args: { value1: 300, value2: "Other value" },
},
],
},
},
]);
const openMenu = (id: string | undefined) => {
fetchNui<ContextMenuProps>("openContext", id);
};
const ContextMenu: React.FC = () => {
const [visible, setVisible] = useState(false);
const [contextMenu, setContextMenu] = useState<ContextMenuProps>({
title: "",
options: { "": { description: "", metadata: [] } },
});
const closeContext = () => {
setVisible(false);
fetchNui("closeContext");
};
// Hides the context menu on ESC
useEffect(() => {
if (!visible) return;
const keyHandler = (e: KeyboardEvent) => {
if (["Escape"].includes(e.code)) closeContext();
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
}, [visible]);
useNuiEvent("hideContext", () => setVisible(false));
useNuiEvent<ContextMenuProps>("showContext", async (data) => {
if (visible) {
setVisible(false);
await new Promise((resolve) => setTimeout(resolve, 100));
}
setContextMenu(data);
setVisible(true);
});
return (
<Flex
position="absolute"
w="75%"
h="80%"
justifyContent="flex-end"
alignItems="center"
>
<ScaleFade in={visible} unmountOnExit>
<Box w="xs" h={580}>
<Flex justifyContent="center" alignItems="center" mb={3}>
{contextMenu.menu && (
<Flex
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%">
<Text
fontFamily="Poppins"
fontSize="md"
p={2}
textAlign="center"
fontWeight="light"
>
{contextMenu.title}
</Text>
</Box>
<Flex
borderRadius="md"
bg="gray.800"
flex="1 15%"
alignSelf="stretch"
textAlign="center"
justifyContent="center"
alignItems="center"
marginLeft={2}
p={2}
_hover={{ bg: "gray.700" }}
transition="300ms"
onClick={() => closeContext()}
>
<FontAwesomeIcon icon="xmark" fontSize={20} />
</Flex>
</Flex>
<Box maxH={560} overflowY="scroll">
{Object.entries(contextMenu.options).map((option, index) => (
<Item option={option} key={`context-item-${index}`} />
))}
</Box>
</Box>
</ScaleFade>
</Flex>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,147 @@
import {
Portal,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
Box,
Text,
Flex,
Spacer,
Image,
HStack,
} from "@chakra-ui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Option, ContextMenuProps } from "../../../interfaces/context";
import { fetchNui } from "../../../utils/fetchNui";
const openMenu = (id: string | undefined) => {
fetchNui<ContextMenuProps>("openContext", id);
};
const clickContext = (id: string) => {
fetchNui("clickContext", id);
};
const Item: React.FC<{
option: [string, Option];
}> = ({ option }) => {
return (
<>
<Popover
placement="right-start"
trigger="hover"
eventListeners={{ scroll: true }}
isLazy
>
<PopoverTrigger>
<Box
bg="gray.800"
borderRadius="md"
h="fit-content"
w="100%"
p={2}
mb={1}
fontFamily="Poppins"
fontSize="md"
transition="300ms"
_hover={{ bg: "gray.700" }}
>
<Flex
w="100%"
alignItems="center"
onClick={() =>
option[1].menu
? openMenu(option[1].menu)
: clickContext(option[0])
}
>
{option[1]?.icon && (
<FontAwesomeIcon
fixedWidth
icon={option[1].icon}
fontSize={20}
style={{
marginRight: 10,
justifySelf: "center",
color: option[1].iconColor,
}}
/>
)}
<Box>
<Box paddingBottom={option[1].description ? 1 : 0}>
<Text w="100%" fontWeight="medium">
{option[1].title ? option[1].title : option[0]}
</Text>
</Box>
{option[1].description && (
<Box paddingBottom={1}>
<Text>{option[1].description}</Text>
</Box>
)}
</Box>
{(option[1].menu || option[1].arrow) && (
<>
<Spacer />
<Box
alignSelf="center"
justifySelf="center"
mr={4}
fontSize="xl"
>
<FontAwesomeIcon icon="chevron-right" />
</Box>
</>
)}
</Flex>
<Portal>
{(option[1].metadata || option[1].image) && (
<PopoverContent
fontFamily="Poppins"
bg="gray.800"
outline="none"
border="none"
w="fit-content"
maxW="2xs"
>
<PopoverBody>
<>
{option[1].image && <Image src={option[1].image} />}
{Array.isArray(option[1].metadata) ? (
option[1].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 option[1].metadata === "object" &&
Object.entries(option[1].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;