mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-17 06:56:03 +01:00
@@ -33,10 +33,14 @@ dependencies {
|
||||
'/onesync',
|
||||
}
|
||||
|
||||
ui_page 'web/build/index.html'
|
||||
|
||||
files {
|
||||
'init.lua',
|
||||
'imports/**/client.lua',
|
||||
'imports/**/shared.lua',
|
||||
'web/build/index.html',
|
||||
'web/build/**/*'
|
||||
}
|
||||
|
||||
shared_script 'resource/main.lua'
|
||||
|
||||
42
resource/interface/client/context.lua
Normal file
42
resource/interface/client/context.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
local contextMenus = {}
|
||||
|
||||
function lib.showContext(id)
|
||||
local data = contextMenus[id]
|
||||
SetNuiFocus(true, true)
|
||||
SendNUIMessage({
|
||||
action = 'showContext',
|
||||
data = {
|
||||
title = data.title,
|
||||
menu = data.menu,
|
||||
options = data.options
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
function lib.registerContext(context)
|
||||
for k, v in pairs(context) do
|
||||
if type(k) == 'number' then
|
||||
contextMenus[v.id] = v
|
||||
else
|
||||
contextMenus[context.id] = context
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNUICallback('openContext', function(id)
|
||||
lib.showContext(id)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('clickContext', function(data)
|
||||
if data.event then TriggerEvent(data.event, data.args) end
|
||||
if data.serverEvent then TriggerServerEvent(data.serverEvent, data.args) end
|
||||
SetNuiFocus(false, false)
|
||||
SendNUIMessage({
|
||||
action = 'hideContext'
|
||||
})
|
||||
end)
|
||||
|
||||
RegisterNUICallback('closeContext', function()
|
||||
SetNuiFocus(false, false)
|
||||
end)
|
||||
19
resource/interface/client/notify.lua
Normal file
19
resource/interface/client/notify.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
--[[```lua
|
||||
{
|
||||
id?: string
|
||||
title?: string
|
||||
description: string
|
||||
duration?: number
|
||||
position?: 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left'
|
||||
style?: table
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
}
|
||||
```]]
|
||||
---@param data table
|
||||
function lib.notify(data)
|
||||
SendNUIMessage({
|
||||
action = 'customNotify',
|
||||
data = data
|
||||
})
|
||||
end
|
||||
23
web/.gitignore
vendored
Normal file
23
web/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
24
web/craco.config.js
Normal file
24
web/craco.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const path = require("path");
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
// Because CEF has issues with loading source maps properly atm,
|
||||
// lets use the best we can get in line with `eval-source-map`
|
||||
if (webpackConfig.mode === 'development' && process.env.IN_GAME_DEV) {
|
||||
webpackConfig.devtool = 'eval-source-map'
|
||||
webpackConfig.output.path = path.join(__dirname, 'build')
|
||||
}
|
||||
|
||||
return webpackConfig
|
||||
}
|
||||
},
|
||||
|
||||
devServer: (devServerConfig) => {
|
||||
if (process.env.IN_GAME_DEV) {
|
||||
// Used for in-game dev mode
|
||||
devServerConfig.devMiddleware.writeToDisk = true
|
||||
}
|
||||
|
||||
return devServerConfig
|
||||
}
|
||||
}
|
||||
66
web/package.json
Normal file
66
web/package.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"homepage": "web/build",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.8.6",
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^12.1.4",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"framer-motion": "^6.2.8",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-markdown": "^8.0.1",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.6.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PUBLIC_URL=/ craco start",
|
||||
"start:game": "cross-env IN_GAME_DEV=1 craco start",
|
||||
"build": "rimraf build && craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": ">=7.0.0 <8.0.0",
|
||||
"@babel/plugin-syntax-flow": "^7.14.5",
|
||||
"@babel/plugin-transform-react-jsx": "^7.14.9",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@testing-library/dom": "^8.11.4",
|
||||
"autoprefixer": "^10.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"postcss": "^8.1.0",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
||||
11434
web/pnpm-lock.yaml
generated
Normal file
11434
web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
web/public/index.html
Normal file
23
web/public/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>NUI React Boilerplate</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
21
web/src/App.tsx
Normal file
21
web/src/App.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import Notifications from "./features/notifications/NotificationWrapper";
|
||||
import CircleProgressbar from "./features/progress/CircleProgressbar";
|
||||
import Progressbar from "./features/progress/Progressbar";
|
||||
import TextUI from "./features/textui/TextUI";
|
||||
import InputDialog from "./features/dialog/InputDialog";
|
||||
import ContextMenu from "./features/menu/ContextMenu";
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Progressbar />
|
||||
<CircleProgressbar />
|
||||
<Notifications />
|
||||
<TextUI />
|
||||
<InputDialog />
|
||||
<ContextMenu />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
101
web/src/features/dialog/InputDialog.tsx
Normal file
101
web/src/features/dialog/InputDialog.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Box,
|
||||
Input,
|
||||
Text,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import { useNuiEvent } from "../../hooks/useNuiEvent";
|
||||
// import { debugData } from "../utils/debugData";
|
||||
import { fetchNui } from "../../utils/fetchNui";
|
||||
|
||||
interface Props {
|
||||
heading: string;
|
||||
inputs: string[];
|
||||
}
|
||||
|
||||
// debugData([
|
||||
// {
|
||||
// action: "openDialog",
|
||||
// data: {
|
||||
// heading: "Police locker",
|
||||
// inputs: ["Locker number", "Locker PIN"],
|
||||
// },
|
||||
// },
|
||||
// ]);
|
||||
|
||||
const InputDialog: React.FC = () => {
|
||||
const [inputOptions, setInputOptions] = React.useState<Props>({
|
||||
heading: "",
|
||||
inputs: [""],
|
||||
});
|
||||
const [inputData, setInputData] = React.useState<string[]>([]);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
useNuiEvent<Props>("openDialog", (data) => {
|
||||
setInputOptions(data);
|
||||
setVisible(true);
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
fetchNui("inputData");
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) => {
|
||||
setInputData((previousData) => {
|
||||
previousData[index] = e.target.value;
|
||||
return previousData;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setVisible(false);
|
||||
fetchNui("inputData", inputData);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={visible}
|
||||
onClose={handleClose}
|
||||
isCentered
|
||||
closeOnEsc
|
||||
closeOnOverlayClick={false}
|
||||
size="xs"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader textAlign="center">{inputOptions.heading}</ModalHeader>
|
||||
<ModalBody>
|
||||
{inputOptions.inputs.map((input: string, index: number) => (
|
||||
<Box mb={3} key={`input-${index}`}>
|
||||
<Text>{input}</Text>
|
||||
<Input onChange={(e) => handleChange(e, index)} />
|
||||
</Box>
|
||||
))}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button colorScheme="blue" onClick={handleConfirm}>
|
||||
Confirm
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputDialog;
|
||||
132
web/src/features/menu/ContextMenu.tsx
Normal file
132
web/src/features/menu/ContextMenu.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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";
|
||||
import Item from "./Item";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { fetchNui } from "../../utils/fetchNui";
|
||||
|
||||
debugData<ContextMenuProps>([
|
||||
{
|
||||
action: "showContext",
|
||||
data: {
|
||||
title: "Vehicle garage",
|
||||
options: {
|
||||
"Dinka Blista": {
|
||||
description: "Super cool vehicle",
|
||||
menu: "some_other_identifier",
|
||||
metadata: {
|
||||
Plate: "KLT 192",
|
||||
Status: "In garage",
|
||||
Health: "30%",
|
||||
},
|
||||
},
|
||||
"Elegy Nitro": {
|
||||
description: "Even cooler vehicle",
|
||||
metadata: ["Plate: JGM 971", "Status: In garage"],
|
||||
},
|
||||
Burger: {
|
||||
description: "Make a delicious burger",
|
||||
metadata: {
|
||||
Bun: 3,
|
||||
Lettuce: 2,
|
||||
Meat: 1,
|
||||
Tomato: 1,
|
||||
Cheese: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
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: [] } },
|
||||
});
|
||||
|
||||
// Hides the context menu on ESC
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (["Escape"].includes(e.code)) {
|
||||
setVisible(false);
|
||||
fetchNui("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 && (
|
||||
<Box
|
||||
borderRadius="md"
|
||||
bg="gray.800"
|
||||
h="100%"
|
||||
flex="1 15%"
|
||||
textAlign="center"
|
||||
marginRight={2}
|
||||
p={2}
|
||||
_hover={{ bg: "gray.700" }}
|
||||
transition="300ms"
|
||||
onClick={() => openMenu(contextMenu.menu)}
|
||||
>
|
||||
<FontAwesomeIcon icon="chevron-left" />
|
||||
</Box>
|
||||
)}
|
||||
<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>
|
||||
<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;
|
||||
134
web/src/features/menu/Item.tsx
Normal file
134
web/src/features/menu/Item.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Portal,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
Box,
|
||||
Text,
|
||||
Flex,
|
||||
Spacer,
|
||||
} from "@chakra-ui/react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Option, ContextMenuProps } from "../../interfaces";
|
||||
import { fetchNui } from "../../utils/fetchNui";
|
||||
|
||||
interface DataProps {
|
||||
event?: string;
|
||||
serverEvent?: string;
|
||||
args?: any;
|
||||
}
|
||||
|
||||
const openMenu = (id: string | undefined) => {
|
||||
fetchNui<ContextMenuProps>("openContext", id);
|
||||
};
|
||||
|
||||
const clickContext = (data: DataProps) => {
|
||||
fetchNui("clickContext", data);
|
||||
};
|
||||
|
||||
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%"
|
||||
onClick={() =>
|
||||
option[1].menu
|
||||
? openMenu(option[1].menu)
|
||||
: clickContext({
|
||||
event: option[1].event,
|
||||
serverEvent: option[1].serverEvent,
|
||||
args: option[1].args,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Box paddingBottom={1}>
|
||||
<Text w="100%" fontWeight="medium">
|
||||
{option[0]}
|
||||
</Text>
|
||||
</Box>
|
||||
{option[1].description && (
|
||||
<Box paddingBottom={1}>
|
||||
<Text>{option[1].description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{option[1].menu && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Box
|
||||
alignSelf="center"
|
||||
justifySelf="center"
|
||||
mr={4}
|
||||
fontSize="xl"
|
||||
>
|
||||
<FontAwesomeIcon icon="chevron-right" />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Portal>
|
||||
{option[1].metadata && (
|
||||
<PopoverContent
|
||||
fontFamily="Poppins"
|
||||
bg="gray.800"
|
||||
outline="none"
|
||||
border="none"
|
||||
w="fit-content"
|
||||
maxW="2xs"
|
||||
>
|
||||
<PopoverBody>
|
||||
{Array.isArray(option[1].metadata) ? (
|
||||
option[1].metadata.map(
|
||||
(metadata: string, index: number) => (
|
||||
<Text key={`context-metadata-${index}`}>
|
||||
{metadata}
|
||||
</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;
|
||||
120
web/src/features/notifications/NotificationWrapper.tsx
Normal file
120
web/src/features/notifications/NotificationWrapper.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
useToast,
|
||||
type ToastPositionWithLogical,
|
||||
Box,
|
||||
HStack,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { useNuiEvent } from "../../hooks/useNuiEvent";
|
||||
import { debugData } from "../../utils/debugData";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description: string;
|
||||
duration?: number;
|
||||
position?: ToastPositionWithLogical;
|
||||
status?: "info" | "warning" | "success" | "error";
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface CustomProps {
|
||||
style?: React.CSSProperties;
|
||||
description: string;
|
||||
title?: string;
|
||||
duration?: number;
|
||||
icon?: IconProp;
|
||||
iconColor?: string;
|
||||
position?: ToastPositionWithLogical;
|
||||
id?: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
debugData<Props>([
|
||||
{
|
||||
action: "notify",
|
||||
data: {
|
||||
description: "Dunak is nerd",
|
||||
title: "Dunak",
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
debugData<CustomProps>([
|
||||
{
|
||||
action: "customNotify",
|
||||
data: {
|
||||
description: "Dunak is nerd",
|
||||
title: "Dunak",
|
||||
icon: "basket-shopping",
|
||||
style: {
|
||||
backgroundColor: "#2D3748",
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const Notifications: React.FC = () => {
|
||||
const toast = useToast();
|
||||
|
||||
useNuiEvent<CustomProps>("customNotify", (data) => {
|
||||
if (data.id && toast.isActive(data.id)) return;
|
||||
if (!data.icon) {
|
||||
//@ts-expect-error because amazing types
|
||||
data.icon =
|
||||
data.type === "error"
|
||||
? "fa-circle-xmark"
|
||||
: data.type === "success"
|
||||
? "fa-circle-check"
|
||||
: "fa-circle-info";
|
||||
}
|
||||
|
||||
toast({
|
||||
duration: data.duration || 3000,
|
||||
position: data.position || "top-right",
|
||||
render: () => (
|
||||
<Box
|
||||
className={`toast-${data.type || "inform"}`}
|
||||
style={data.style}
|
||||
p={2}
|
||||
borderRadius="sm"
|
||||
boxShadow="md"
|
||||
>
|
||||
<HStack spacing={0}>
|
||||
{
|
||||
<FontAwesomeIcon
|
||||
//@ts-expect-error because amazing types
|
||||
icon={data.icon}
|
||||
fontSize="1.3em"
|
||||
style={{ paddingRight: 8 }}
|
||||
color={data.iconColor}
|
||||
/>
|
||||
}
|
||||
<Box w="100%">
|
||||
{data.title && <Text as="b">{data.title}</Text>}
|
||||
{data.description && <Text>{data.description}</Text>}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
useNuiEvent<Props>("notify", (data) => {
|
||||
if (data.id && toast.isActive(data.id)) return;
|
||||
toast({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
duration: data.duration || 4000,
|
||||
position: data.position || "top-right",
|
||||
status: data.status,
|
||||
});
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
110
web/src/features/progress/CircleProgressbar.tsx
Normal file
110
web/src/features/progress/CircleProgressbar.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import {
|
||||
CircularProgress,
|
||||
CircularProgressLabel,
|
||||
Flex,
|
||||
ScaleFade,
|
||||
} from "@chakra-ui/react";
|
||||
import { useNuiEvent } from "../../hooks/useNuiEvent";
|
||||
import { debugData } from "../../utils/debugData";
|
||||
import { fetchNui } from "../../utils/fetchNui";
|
||||
|
||||
interface Props {
|
||||
duration: number;
|
||||
position?: "middle" | "bottom";
|
||||
percent?: boolean;
|
||||
}
|
||||
|
||||
debugData([
|
||||
{
|
||||
action: "circleProgress",
|
||||
data: {
|
||||
duration: 8000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const CircleProgressbar: React.FC = () => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [progressDuration, setProgressDuration] = React.useState(0);
|
||||
const [position, setPosition] = React.useState<"middle" | "bottom">("middle");
|
||||
const [value, setValue] = React.useState(0);
|
||||
const [cancelled, setCancelled] = React.useState(false);
|
||||
|
||||
const progressComplete = () => {
|
||||
setVisible(false);
|
||||
fetchNui("progressComplete");
|
||||
};
|
||||
|
||||
const progressCancel = () => {
|
||||
setCancelled(true);
|
||||
setValue(99); // Sets the final value to 100% kek
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
useNuiEvent("progressCancel", progressCancel);
|
||||
|
||||
useNuiEvent<Props>("circleProgress", (data) => {
|
||||
if (visible) return;
|
||||
setCancelled(false);
|
||||
setVisible(true);
|
||||
setValue(0);
|
||||
setProgressDuration(data.duration);
|
||||
setPosition(data.position || "middle");
|
||||
const onePercent = data.duration * 0.01;
|
||||
const updateProgress = setInterval(() => {
|
||||
setValue((previousValue) => {
|
||||
const newValue = previousValue + 1;
|
||||
newValue >= 100 && clearInterval(updateProgress);
|
||||
return newValue;
|
||||
});
|
||||
}, onePercent);
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
h={position === "middle" ? "100%" : "20%"}
|
||||
w="100%"
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<ScaleFade in={visible} unmountOnExit>
|
||||
<CircularProgress
|
||||
value={value}
|
||||
size="5rem"
|
||||
trackColor="rgba(0, 0, 0, 0.6)"
|
||||
onAnimationEnd={progressComplete}
|
||||
thickness={6}
|
||||
color={cancelled ? "rgb(198, 40, 40)" : "white"}
|
||||
sx={
|
||||
!cancelled
|
||||
? {
|
||||
".chakra-progress__indicator": {
|
||||
transition: "none !important",
|
||||
animation: "progress linear forwards !important",
|
||||
animationDuration: `${progressDuration}ms !important`,
|
||||
opacity: "1 !important",
|
||||
},
|
||||
}
|
||||
: {
|
||||
".chakra-progress__indicator": {
|
||||
transition: "none !important",
|
||||
strokeDasharray: "264, 0 !important", // sets circle to full
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CircularProgressLabel fontFamily="Fira Mono">
|
||||
{value}%
|
||||
</CircularProgressLabel>
|
||||
</CircularProgress>
|
||||
</ScaleFade>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircleProgressbar;
|
||||
102
web/src/features/progress/Progressbar.tsx
Normal file
102
web/src/features/progress/Progressbar.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
Flex,
|
||||
Box,
|
||||
ScaleFade,
|
||||
} from "@chakra-ui/react";
|
||||
import { useNuiEvent } from "../../hooks/useNuiEvent";
|
||||
import { debugData } from "../../utils/debugData";
|
||||
import { fetchNui } from "../../utils/fetchNui";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
debugData([
|
||||
{
|
||||
action: "progress",
|
||||
data: {
|
||||
label: "Using Lockpick",
|
||||
duration: 8000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const Progressbar: React.FC = () => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [label, setLabel] = React.useState("");
|
||||
const [duration, setDuration] = React.useState(0);
|
||||
const [cancelled, setCancelled] = React.useState(false);
|
||||
|
||||
const progressComplete = () => {
|
||||
setVisible(false);
|
||||
fetchNui("progressComplete");
|
||||
};
|
||||
|
||||
const progressCancel = () => {
|
||||
setCancelled(true);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
useNuiEvent("progressCancel", progressCancel);
|
||||
|
||||
useNuiEvent<Props>("progress", (data) => {
|
||||
if (visible) return;
|
||||
setCancelled(false);
|
||||
setVisible(true);
|
||||
setLabel(data.label);
|
||||
setDuration(data.duration);
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
h="20%"
|
||||
w="100%"
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box width="22rem">
|
||||
<ScaleFade in={visible} unmountOnExit>
|
||||
<Progress
|
||||
value={100}
|
||||
height="2.8rem"
|
||||
borderRadius="sm"
|
||||
fontFamily="Inter"
|
||||
flex="1 1 auto"
|
||||
boxShadow="lg"
|
||||
onAnimationEnd={progressComplete}
|
||||
sx={
|
||||
!cancelled
|
||||
? {
|
||||
// really scuffed solution but works, wonder if there's a better way to do this?
|
||||
"> div:first-of-type": {
|
||||
animation: `progress-bar linear ${duration}ms`,
|
||||
borderRadius: "none",
|
||||
},
|
||||
}
|
||||
: {
|
||||
"> div:first-of-type": {
|
||||
width: "100%",
|
||||
backgroundColor: "rgb(198, 40, 40)",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<ProgressLabel fontSize={22} fontWeight="light">
|
||||
{label}
|
||||
</ProgressLabel>
|
||||
</Progress>
|
||||
</ScaleFade>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Progressbar;
|
||||
84
web/src/features/textui/TextUI.tsx
Normal file
84
web/src/features/textui/TextUI.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useNuiEvent } from "../../hooks/useNuiEvent";
|
||||
import { Box, Flex, ScaleFade } from "@chakra-ui/react";
|
||||
import { debugData } from "../../utils/debugData";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
position?: "right-center" | "left-center" | "top-center";
|
||||
icon?: IconProp;
|
||||
iconColor?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
debugData([
|
||||
{
|
||||
action: "textUi",
|
||||
data: {
|
||||
text: "[E] - Access locker inventory \n [G] - Do something else",
|
||||
position: "right-center",
|
||||
icon: "door-open",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const TextUI: React.FC = () => {
|
||||
const [data, setData] = React.useState<Props>({
|
||||
text: "",
|
||||
position: "right-center",
|
||||
});
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
useNuiEvent<Props>("textUi", (data) => {
|
||||
setData(data);
|
||||
setVisible(true);
|
||||
});
|
||||
|
||||
useNuiEvent("textUiHide", () => setVisible(false));
|
||||
|
||||
return (
|
||||
<Flex
|
||||
w="100%"
|
||||
h="100%"
|
||||
p={3}
|
||||
position="absolute"
|
||||
alignItems={data.position === "top-center" ? "baseline" : "center"}
|
||||
justifyContent={
|
||||
data.position === "right-center"
|
||||
? "flex-end"
|
||||
: data.position === "left-center"
|
||||
? "flex-start"
|
||||
: "center"
|
||||
}
|
||||
>
|
||||
<ScaleFade in={visible} unmountOnExit>
|
||||
<Box
|
||||
bg="gray.700"
|
||||
boxShadow="lg"
|
||||
p={3}
|
||||
fontFamily="Poppins"
|
||||
style={data.style}
|
||||
borderRadius="md"
|
||||
maxW="xs"
|
||||
>
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
{data.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={data.icon}
|
||||
color={data.iconColor}
|
||||
fontSize="1.4rem"
|
||||
style={{ paddingRight: 15 }}
|
||||
/>
|
||||
)}
|
||||
<ReactMarkdown>{data.text}</ReactMarkdown>
|
||||
</Flex>
|
||||
</Box>
|
||||
</ScaleFade>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextUI;
|
||||
49
web/src/hooks/useNuiEvent.ts
Normal file
49
web/src/hooks/useNuiEvent.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {MutableRefObject, useEffect, useRef} from "react";
|
||||
import {noop} from "../utils/misc";
|
||||
|
||||
interface NuiMessageData<T = unknown> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
type NuiHandlerSignature<T> = (data: T) => void;
|
||||
|
||||
/**
|
||||
* A hook that manage events listeners for receiving data from the client scripts
|
||||
* @param action The specific `action` that should be listened for.
|
||||
* @param handler The callback function that will handle data relayed by this hook
|
||||
*
|
||||
* @example
|
||||
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
|
||||
* // whatever logic you want
|
||||
* })
|
||||
*
|
||||
**/
|
||||
|
||||
export const useNuiEvent = <T = any>(
|
||||
action: string,
|
||||
handler: (data: T) => void
|
||||
) => {
|
||||
const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);
|
||||
|
||||
// Make sure we handle for a reactive handler
|
||||
useEffect(() => {
|
||||
savedHandler.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = (event: MessageEvent<NuiMessageData<T>>) => {
|
||||
const { action: eventAction, data } = event.data;
|
||||
|
||||
if (savedHandler.current) {
|
||||
if (eventAction === action) {
|
||||
savedHandler.current(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", eventListener);
|
||||
// Remove Event Listener on component cleanup
|
||||
return () => window.removeEventListener("message", eventListener);
|
||||
}, [action]);
|
||||
};
|
||||
64
web/src/index.css
Normal file
64
web/src/index.css
Normal file
@@ -0,0 +1,64 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;500;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Fira+Mono&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");
|
||||
|
||||
body {
|
||||
background: none !important;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100vh;
|
||||
user-select: none;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% {
|
||||
stroke-dasharray: 0, 264;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 264, 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress-bar {
|
||||
from {
|
||||
width: 0%;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toast-inform {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
43
web/src/index.tsx
Normal file
43
web/src/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { VisibilityProvider } from "./providers/VisibilityProvider";
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import { theme } from "./theme";
|
||||
import { debugData } from "./utils/debugData";
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
import { far } from "@fortawesome/free-regular-svg-icons";
|
||||
import { fab } from "@fortawesome/free-brands-svg-icons";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { isEnvBrowser } from "./utils/misc";
|
||||
|
||||
library.add(fas, far, fab);
|
||||
|
||||
if (isEnvBrowser()) {
|
||||
const root = document.getElementById("root");
|
||||
|
||||
// https://i.imgur.com/iPTAdYV.png - Night time img
|
||||
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
|
||||
root!.style.backgroundSize = "cover";
|
||||
root!.style.backgroundRepeat = "no-repeat";
|
||||
root!.style.backgroundPosition = "center";
|
||||
}
|
||||
|
||||
debugData([
|
||||
{
|
||||
action: "setVisible",
|
||||
data: true,
|
||||
},
|
||||
]);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<VisibilityProvider>
|
||||
<ChakraProvider theme={theme}>
|
||||
<App />
|
||||
</ChakraProvider>
|
||||
</VisibilityProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
18
web/src/interfaces/index.tsx
Normal file
18
web/src/interfaces/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface Option {
|
||||
menu?: string;
|
||||
description?: string;
|
||||
metadata?: string[] | { [key: string]: any };
|
||||
event?: string;
|
||||
serverEvent?: string;
|
||||
args?: any;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
[key: string]: Option;
|
||||
}
|
||||
|
||||
export interface ContextMenuProps {
|
||||
title: string;
|
||||
menu?: string;
|
||||
options: Options;
|
||||
}
|
||||
37
web/src/providers/VisibilityProvider.tsx
Normal file
37
web/src/providers/VisibilityProvider.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { Context, createContext, useContext, useState } from "react";
|
||||
import { useNuiEvent } from "../hooks/useNuiEvent";
|
||||
|
||||
const VisibilityCtx = createContext<VisibilityProviderValue | null>(null);
|
||||
|
||||
interface VisibilityProviderValue {
|
||||
setVisible: (visible: boolean) => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
// This should be mounted at the top level of your application, it is currently set to
|
||||
// apply a CSS visibility value. If this is non-performant, this should be customized.
|
||||
export const VisibilityProvider: React.FC = ({ children }) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useNuiEvent<boolean>("setVisible", setVisible);
|
||||
|
||||
return (
|
||||
<VisibilityCtx.Provider
|
||||
value={{
|
||||
visible,
|
||||
setVisible,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ visibility: visible ? "visible" : "hidden", height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</VisibilityCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useVisibility = () =>
|
||||
useContext<VisibilityProviderValue>(
|
||||
VisibilityCtx as Context<VisibilityProviderValue>
|
||||
);
|
||||
1
web/src/react-app-env.d.ts
vendored
Normal file
1
web/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
22
web/src/theme/index.ts
Normal file
22
web/src/theme/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { extendTheme, type ThemeConfig } from "@chakra-ui/react";
|
||||
|
||||
const config: ThemeConfig = {
|
||||
initialColorMode: 'dark',
|
||||
useSystemColorMode: false
|
||||
}
|
||||
|
||||
export const theme = extendTheme({
|
||||
config,
|
||||
components: {
|
||||
Progress: {
|
||||
baseStyle: {
|
||||
filledTrack: {
|
||||
bg: 'green.400'
|
||||
},
|
||||
track: {
|
||||
bg: 'rgba(0, 0, 0, 0.6)'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
30
web/src/utils/debugData.ts
Normal file
30
web/src/utils/debugData.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {isEnvBrowser} from "./misc";
|
||||
|
||||
interface DebugEvent<T = any> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
|
||||
* This is used when developing in browser
|
||||
*
|
||||
* @param events - The event you want to cover
|
||||
* @param timer - How long until it should trigger (ms)
|
||||
*/
|
||||
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
|
||||
if (process.env.NODE_ENV === "development" && isEnvBrowser()) {
|
||||
for (const event of events) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
action: event.action,
|
||||
data: event.data,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
28
web/src/utils/fetchNui.ts
Normal file
28
web/src/utils/fetchNui.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Simple wrapper around fetch API tailored for CEF/NUI use. This abstraction
|
||||
* can be extended to include AbortController if needed or if the response isn't
|
||||
* JSON. Tailor it to your needs.
|
||||
*
|
||||
* @param eventName - The endpoint eventname to target
|
||||
* @param data - Data you wish to send in the NUI Callback
|
||||
*
|
||||
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
|
||||
*/
|
||||
|
||||
export async function fetchNui<T = any>(eventName: string, data?: any): Promise<T> {
|
||||
const options = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
const resourceName = (window as any).GetParentResourceName ? (window as any).GetParentResourceName() : 'nui-frame-app';
|
||||
|
||||
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
|
||||
|
||||
const respFormatted = await resp.json()
|
||||
|
||||
return respFormatted
|
||||
}
|
||||
6
web/src/utils/misc.ts
Normal file
6
web/src/utils/misc.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Will return whether the current environment is in a regular browser
|
||||
// and not CEF
|
||||
export const isEnvBrowser = (): boolean => !(window as any).invokeNative
|
||||
|
||||
// Basic no operation function
|
||||
export const noop = () => {}
|
||||
26
web/tsconfig.json
Normal file
26
web/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user