Initial commit

This commit is contained in:
Sticks
2024-06-09 20:24:22 -05:00
committed by GitHub
commit f16a32b63e
27 changed files with 845 additions and 0 deletions

18
web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
};

23
web/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
build
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NUI React Boilerplate</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

29
web/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "web",
"homepage": "web/build",
"private": true,
"type": "module",
"version": "0.1.0",
"scripts": {
"start": "vite",
"start:game": "vite build --watch",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.54.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

View File

@ -0,0 +1,26 @@
.nui-wrapper {
text-align: center;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
pre {
counter-reset:line-numbering;
background:#2c3e50;
padding:12px 0px 14px 0;
color:#ecf0f1;
line-height:140%;
}
.popup-thing {
background: #282c34;
border-radius: 10px;
width: 500px;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
color: white;
}

View File

@ -0,0 +1,66 @@
import React, { useState } from "react";
import "./App.css";
import { debugData } from "../utils/debugData";
import { fetchNui } from "../utils/fetchNui";
// This will set the NUI to visible if we are
// developing in browser
debugData([
{
action: "setVisible",
data: true,
},
]);
interface ReturnClientDataCompProps {
data: unknown;
}
const ReturnClientDataComp: React.FC<ReturnClientDataCompProps> = ({
data,
}) => (
<>
<h5>Returned Data:</h5>
<pre>
<code>{JSON.stringify(data, null)}</code>
</pre>
</>
);
interface ReturnData {
x: number;
y: number;
z: number;
}
const App: React.FC = () => {
const [clientData, setClientData] = useState<ReturnData | null>(null);
const handleGetClientData = () => {
fetchNui<ReturnData>("getClientData")
.then((retData) => {
console.log("Got return data from client scripts:");
console.dir(retData);
setClientData(retData);
})
.catch((e) => {
console.error("Setting mock data due to error", e);
setClientData({ x: 500, y: 300, z: 200 });
});
};
return (
<div className="nui-wrapper">
<div className="popup-thing">
<div>
<h1>This is the NUI Popup!</h1>
<p>Exit with the escape key</p>
<button onClick={handleGetClientData}>Get Client Data</button>
{clientData && <ReturnClientDataComp data={clientData} />}
</div>
</div>
</div>
);
};
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 = unknown>(
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]);
};

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

@ -0,0 +1,18 @@
body {
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;
}

13
web/src/main.tsx Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { VisibilityProvider } from './providers/VisibilityProvider';
import App from './components/App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<VisibilityProvider>
<App />
</VisibilityProvider>
</React.StrictMode>,
);

View File

@ -0,0 +1,64 @@
import React, {
Context,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { useNuiEvent } from "../hooks/useNuiEvent";
import { fetchNui } from "../utils/fetchNui";
import { isEnvBrowser } from "../utils/misc";
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: React.ReactNode }> = ({
children,
}) => {
const [visible, setVisible] = useState(false);
useNuiEvent<boolean>("setVisible", setVisible);
// Handle pressing escape/backspace
useEffect(() => {
// Only attach listener when we are visible
if (!visible) return;
const keyHandler = (e: KeyboardEvent) => {
if (["Backspace", "Escape"].includes(e.code)) {
if (!isEnvBrowser()) fetchNui("hideFrame");
else setVisible(!visible);
}
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
}, [visible]);
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>,
);

View File

@ -0,0 +1,30 @@
import { isEnvBrowser } from "./misc";
interface DebugEvent<T = unknown> {
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 (import.meta.env.MODE === "development" && isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
action: event.action,
data: event.data,
},
}),
);
}, timer);
}
}
};

39
web/src/utils/fetchNui.ts Normal file
View File

@ -0,0 +1,39 @@
import { isEnvBrowser } from "./misc";
/**
* 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
* @param mockData - Mock data to be returned if in the browser
*
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
*/
export async function fetchNui<T = unknown>(
eventName: string,
data?: unknown,
mockData?: T,
): Promise<T> {
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
if (isEnvBrowser() && mockData) return mockData;
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 = () => {};

1
web/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
web/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

11
web/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
build: {
outDir: 'build',
},
});