feat(nui): Initial NUI files

This commit is contained in:
Luke
2022-03-13 22:30:57 +01:00
parent 2ce934c41a
commit 5314b59e8b
17 changed files with 10231 additions and 0 deletions

23
web/.gitignore vendored Normal file
View 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
View 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
}
}

54
web/package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "web",
"version": "0.1.0",
"homepage": "web/build",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.8.6",
"@emotion/react": "11",
"@emotion/styled": "11",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^26.0.24",
"@types/node": "^16.11.18",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"framer-motion": "6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"typescript": "^4.5.5",
"web-vitals": "^2.1.3"
},
"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": {
"@craco/craco": "^6.4.3",
"cross-env": "^7.0.3",
"rimraf": "^3.0.2"
}
}

23
web/public/index.html Normal file
View 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>

View File

@@ -0,0 +1,9 @@
const App: React.FC = () => {
return (
<>
<></>
</>
);
};
export default App;

View 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]);
};

19
web/src/index.css Normal file
View File

@@ -0,0 +1,19 @@
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;
}
#root {
height: 100%;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

18
web/src/index.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./components/App";
import { VisibilityProvider } from "./providers/VisibilityProvider";
import { ChakraProvider } from "@chakra-ui/react";
import { theme } from "./theme";
ReactDOM.render(
<React.StrictMode>
<VisibilityProvider>
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
</VisibilityProvider>
</React.StrictMode>,
document.getElementById("root")
);

View 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(false);
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
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

8
web/src/theme/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import { extendTheme, type ThemeConfig } from "@chakra-ui/react";
const config: ThemeConfig = {
initialColorMode: 'dark',
useSystemColorMode: false
}
export const theme = extendTheme({config})

View 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
View 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
View 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
View 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"
]
}

9872
web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff