From f16a32b63e8677620e9232414f564705e89363bd Mon Sep 17 00:00:00 2001 From: Sticks Date: Sun, 9 Jun 2024 20:24:22 -0500 Subject: [PATCH] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 35 ++++ .github/workflows/ci.yml | 31 ++++ .github/workflows/release.yml | 30 ++++ .gitignore | 1 + LICENSE | 19 ++ README.md | 216 +++++++++++++++++++++++ client/client.lua | 25 +++ client/utils.lua | 30 ++++ fxmanifest.lua | 23 +++ server/server.lua | 0 web/.eslintrc.cjs | 18 ++ web/.gitignore | 23 +++ web/index.html | 13 ++ web/package.json | 29 +++ web/src/components/App.css | 26 +++ web/src/components/App.tsx | 66 +++++++ web/src/hooks/useNuiEvent.ts | 49 +++++ web/src/index.css | 18 ++ web/src/main.tsx | 13 ++ web/src/providers/VisibilityProvider.tsx | 64 +++++++ web/src/utils/debugData.ts | 30 ++++ web/src/utils/fetchNui.ts | 39 ++++ web/src/utils/misc.ts | 6 + web/src/vite-env.d.ts | 1 + web/tsconfig.json | 21 +++ web/tsconfig.node.json | 8 + web/vite.config.ts | 11 ++ 27 files changed, 845 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 client/client.lua create mode 100644 client/utils.lua create mode 100644 fxmanifest.lua create mode 100644 server/server.lua create mode 100644 web/.eslintrc.cjs create mode 100644 web/.gitignore create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/src/components/App.css create mode 100644 web/src/components/App.tsx create mode 100644 web/src/hooks/useNuiEvent.ts create mode 100644 web/src/index.css create mode 100644 web/src/main.tsx create mode 100644 web/src/providers/VisibilityProvider.tsx create mode 100644 web/src/utils/debugData.ts create mode 100644 web/src/utils/fetchNui.ts create mode 100644 web/src/utils/misc.ts create mode 100644 web/src/vite-env.d.ts create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4afbbb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug Report +about: Create a bug report to help solve an issue +title: 'Bug: TITLE' +labels: New Issue +assignees: '' +--- + +**Describe the issue** + +A clear and concise description of what the bug is. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**To Reproduce** + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + + +**Media** + +If applicable, add a screenshot or a video to help explain your problem. + +**Needed information (please complete the following information):** +- **Client Version:**: [e.g. Canary or Release] +- **Template Version**: [e.g. 3486] Don't know?~~Check the version in your package.json~~ + +**Additional context** +Add any other context about the issue here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4fa52ae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Main CI +on: [push, pull_request] +jobs: + build: + name: Build Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup node environment + uses: actions/setup-node@v2 + with: + node-version: 20.x + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn config get cacheFolder)" + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install deps + run: yarn --frozen-lockfile + - name: Try build + run: yarn build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6ff9176 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Tagged Release +on: + push: + tags: + - "v*" +jobs: + create-tagged-release: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ github.ref }} + - name: Get tag + run: echo ::set-output name=VERSION_TAG::${GITHUB_REF/refs\/tags\//} + id: get_tag + - name: 'Setup Node.js' + uses: 'actions/setup-node@v1' + with: + node-version: 20.x + - name: Create release + uses: marvinpinto/action-automatic-releases@latest + with: + title: React/Lua Boilerplate - ${{ steps.get_tag.outputs.VERSION_TAG }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + id: auto_release + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c657199 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Project Error + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..227d79d --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +
+ Material-UI logo +
+

FiveM React and Lua Boilerplate

+ +
+A simple and extendable React (TypeScript) boilerplate designed around the Lua ScRT +
+ +
+ +[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/project-error/pe-utils/master/LICENSE) +![Discord](https://img.shields.io/discord/791854454760013827?label=Our%20Discord) +![David](https://img.shields.io/david/project-error/fivem-react-boilerplate-lua) +[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=project-error/fivem-react-boilerplate-lua)](https://dependabot.com) +
+ +This repository is a basic boilerplate for getting started +with React in NUI. It contains several helpful utilities and +is bootstrapped using `create-react-app`. It is for both browser +and in-game based development workflows. + +For in-game workflows, Utilizing `craco` to override CRA, we can have hot +builds that just require a resource restart instead of a full +production build + +This version of the boilerplate is meant for the CfxLua runtime. + +## Requirements +* [Node > v10.6](https://nodejs.org/en/) +* [Yarn](https://yarnpkg.com/getting-started/install) (Preferred but not required) + +*A basic understanding of the modern web development workflow. If you don't +know this yet, React might not be for you just yet.* + +## Getting Started + +First clone the repository or use the template option and place +it within your `resources` folder + +### Installation + +*The boilerplate was made using `yarn` but is still compatible with +`npm`.* + +Install dependencies by navigating to the `web` folder within +a terminal of your choice and type `npm i` or `yarn`. + +## Features + +This boilerplate comes with some utilities and examples to work off of. + +### Lua Utils + +**SendReactMessage** + +This is a small wrapper for dispatching NUI messages. This is designed +to be used with the `useNuiEvent` React hook. + +Signature +```lua +---@param action string The action you wish to target +---@param data any The data you wish to send along with this action +SendReactMessage(action, data) +``` + +Usage +```lua +SendReactMessage('setVisible', true) +``` + +**debugPrint** + +A debug printing utility that is dependent on a convar, +if the convar is set this will print out to the console. + +The convar is dependent on the name given to the resource. +It follows this format `YOUR_RESOURCE_NAME-debugMode` + +To turn on debugMode add `setr YOUR_RESOURCE_NAME-debugMode 1` to +your server.cfg or use the `setr` console command instead. + +Signature (Replicates `print`) +```lua +---@param ... any[] The arguments you wish to send +debugPrint(...) +``` + +Usage +```lua +debugPrint('wow cool string to print', true, someOtherVar) +``` + +### React Utils + +Signatures are not included for these utilities as the type definitions +are sufficient enough. + +**useNuiEvent** + +This is a custom React hook that is designed to intercept and handle +messages dispatched by the game scripts. This is the primary +way of creating passive listeners. + + +*Note: For now handlers can only be registered a single time. I haven't +come across a personal usecase for a cascading event system* + +**Usage** +```jsx +const MyComp: React.FC = () => { + const [state, setState] = useState('') + + useNuiEvent('myAction', (data) => { + // the first argument to the handler function + // is the data argument sent using SendReactMessage + + // do whatever logic u want here + setState(data) + }) + + return( +
+

Some component

+

{state}

+
+ ) +} + +``` + +**fetchNui** + +This is a simple NUI focused wrapper around the standard `fetch` API. +This is the main way to accomplish active NUI data fetching +or to trigger NUI callbacks in the game scripts. + +When using this, you must always at least callback using `{}` +in the gamescripts. + +*This can be heavily customized to your use case* + +**Usage** +```ts +// First argument is the callback event name. +fetchNui('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}) +}) +``` + +**debugData** + +This is a function allowing for mocking dispatched game script +actions in a browser environment. It will trigger `useNuiEvent` handlers +as if they were dispatched by the game scripts. **It will only fire if the current +environment is a regular browser and not CEF** + +**Usage** +```ts +// This will target the useNuiEvent hooks registered with `setVisible` +// and pass them the data of `true` +debugData([ + { + action: 'setVisible', + data: true, + } +]) +``` + +**Misc Utils** + +These are small but useful included utilities. + +* `isEnvBrowser()` - Will return a boolean indicating if the current + environment is a regular browser. (Useful for logic in development) + +## Development Workflow + +This boilerplate was designed with development workflow in mind. +It includes some helpful scripts to accomplish that. + +**Hot Builds In-Game** + +When developing in-game, you can use the hot build system by +running the `start:game` script. This is essentially the start +script but it writes to disk. Meaning all that is required is a +resource restart to update the game script + +**Usage** +```sh +# yarn +yarn start:game +# npm +npm run start:game +``` + +**Production Builds** + +When you are done with development phase for your resource. You +must create a production build that is optimized and minimized. + +You can do this by running the following: + +```sh +npm run build +yarn build +``` + +## Additional Notes + +Need further support? Join our [Discord](https://discord.com/invite/HYwBjTbAY5)! diff --git a/client/client.lua b/client/client.lua new file mode 100644 index 0000000..9bb1eca --- /dev/null +++ b/client/client.lua @@ -0,0 +1,25 @@ +local function toggleNuiFrame(shouldShow) + SetNuiFocus(shouldShow, shouldShow) + SendReactMessage('setVisible', shouldShow) +end + +RegisterCommand('show-nui', function() + toggleNuiFrame(true) + debugPrint('Show NUI frame') +end) + +RegisterNUICallback('hideFrame', function(_, cb) + toggleNuiFrame(false) + debugPrint('Hide NUI frame') + cb({}) +end) + +RegisterNUICallback('getClientData', function(data, cb) + debugPrint('Data sent by React', json.encode(data)) + +-- Lets send back client coords to the React frame for use + local curCoords = GetEntityCoords(PlayerPedId()) + + local retData = { x = curCoords.x, y = curCoords.y, z = curCoords.z } + cb(retData) +end) \ No newline at end of file diff --git a/client/utils.lua b/client/utils.lua new file mode 100644 index 0000000..d78b5f4 --- /dev/null +++ b/client/utils.lua @@ -0,0 +1,30 @@ +--- A simple wrapper around SendNUIMessage that you can use to +--- dispatch actions to the React frame. +--- +---@param action string The action you wish to target +---@param data any The data you wish to send along with this action +function SendReactMessage(action, data) + SendNUIMessage({ + action = action, + data = data + }) +end + +local currentResourceName = GetCurrentResourceName() + +local debugIsEnabled = GetConvarInt(('%s-debugMode'):format(currentResourceName), 0) == 1 + +--- A simple debug print function that is dependent on a convar +--- will output a nice prettfied message if debugMode is on +function debugPrint(...) + if not debugIsEnabled then return end + local args = { ... } + + local appendStr = '' + for _, v in ipairs(args) do + appendStr = appendStr .. ' ' .. tostring(v) + end + local msgTemplate = '^3[%s]^0%s' + local finalMsg = msgTemplate:format(currentResourceName, appendStr) + print(finalMsg) +end diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..53c96b6 --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,23 @@ +fx_version "cerulean" + +description "Basic React (TypeScript) & Lua Game Scripts Boilerplate" +author "Project Error" +version '1.0.0' +repository 'https://github.com/project-error/fivem-react-boilerplate-lua' + +lua54 'yes' + +games { + "gta5", + "rdr3" +} + +ui_page 'web/build/index.html' + +client_script "client/**/*" +server_script "server/**/*" + +files { + 'web/build/index.html', + 'web/build/**/*', +} \ No newline at end of file diff --git a/server/server.lua b/server/server.lua new file mode 100644 index 0000000..e69de29 diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 0000000..6e8698b --- /dev/null +++ b/web/.eslintrc.cjs @@ -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 }, + ], + }, +}; diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..317c8cc --- /dev/null +++ b/web/.gitignore @@ -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? diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b216a76 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + NUI React Boilerplate + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..57e471b --- /dev/null +++ b/web/package.json @@ -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" + } +} diff --git a/web/src/components/App.css b/web/src/components/App.css new file mode 100644 index 0000000..8f56557 --- /dev/null +++ b/web/src/components/App.css @@ -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; +} diff --git a/web/src/components/App.tsx b/web/src/components/App.tsx new file mode 100644 index 0000000..6faaa0d --- /dev/null +++ b/web/src/components/App.tsx @@ -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 = ({ + data, +}) => ( + <> +
Returned Data:
+
+      {JSON.stringify(data, null)}
+    
+ +); + +interface ReturnData { + x: number; + y: number; + z: number; +} + +const App: React.FC = () => { + const [clientData, setClientData] = useState(null); + + const handleGetClientData = () => { + fetchNui("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 ( +
+
+
+

This is the NUI Popup!

+

Exit with the escape key

+ + {clientData && } +
+
+
+ ); +}; + +export default App; diff --git a/web/src/hooks/useNuiEvent.ts b/web/src/hooks/useNuiEvent.ts new file mode 100644 index 0000000..62c7172 --- /dev/null +++ b/web/src/hooks/useNuiEvent.ts @@ -0,0 +1,49 @@ +import { MutableRefObject, useEffect, useRef } from "react"; +import { noop } from "../utils/misc"; + +interface NuiMessageData { + action: string; + data: T; +} + +type NuiHandlerSignature = (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 = ( + action: string, + handler: (data: T) => void, +) => { + const savedHandler: MutableRefObject> = useRef(noop); + + // Make sure we handle for a reactive handler + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + const eventListener = (event: MessageEvent>) => { + 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]); +}; diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..cf3c162 --- /dev/null +++ b/web/src/index.css @@ -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; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..5aa0669 --- /dev/null +++ b/web/src/main.tsx @@ -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( + + + + + , +); diff --git a/web/src/providers/VisibilityProvider.tsx b/web/src/providers/VisibilityProvider.tsx new file mode 100644 index 0000000..d4e270e --- /dev/null +++ b/web/src/providers/VisibilityProvider.tsx @@ -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(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("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 ( + +
+ {children} +
+
+ ); +}; + +export const useVisibility = () => + useContext( + VisibilityCtx as Context, + ); diff --git a/web/src/utils/debugData.ts b/web/src/utils/debugData.ts new file mode 100644 index 0000000..0e03d3e --- /dev/null +++ b/web/src/utils/debugData.ts @@ -0,0 +1,30 @@ +import { isEnvBrowser } from "./misc"; + +interface DebugEvent { + 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 =

(events: DebugEvent

[], 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); + } + } +}; diff --git a/web/src/utils/fetchNui.ts b/web/src/utils/fetchNui.ts new file mode 100644 index 0000000..266b019 --- /dev/null +++ b/web/src/utils/fetchNui.ts @@ -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( + eventName: string, + data?: unknown, + mockData?: T, +): Promise { + 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; +} diff --git a/web/src/utils/misc.ts b/web/src/utils/misc.ts new file mode 100644 index 0000000..f0a087d --- /dev/null +++ b/web/src/utils/misc.ts @@ -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 = () => {}; diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..3d0a51a --- /dev/null +++ b/web/tsconfig.json @@ -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" }] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..e993792 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "esnext", + "moduleResolution": "node" + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..724195e --- /dev/null +++ b/web/vite.config.ts @@ -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', + }, +});