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

FiveM React and Lua Boilerplate

5 | 6 |
7 | A simple and extendable React (TypeScript) boilerplate designed around the Lua ScRT 8 |
9 | 10 |
11 | 12 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/project-error/pe-utils/master/LICENSE) 13 | ![Discord](https://img.shields.io/discord/791854454760013827?label=Our%20Discord) 14 | ![David](https://img.shields.io/david/project-error/fivem-react-boilerplate-lua) 15 | [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=project-error/fivem-react-boilerplate-lua)](https://dependabot.com) 16 |
17 | 18 | This repository is a basic boilerplate for getting started 19 | with React in NUI. It contains several helpful utilities and 20 | is bootstrapped using `create-react-app`. It is for both browser 21 | and in-game based development workflows. 22 | 23 | For in-game workflows, Utilizing `craco` to override CRA, we can have hot 24 | builds that just require a resource restart instead of a full 25 | production build 26 | 27 | This version of the boilerplate is meant for the CfxLua runtime. 28 | 29 | ## Requirements 30 | * [Node > v10.6](https://nodejs.org/en/) 31 | * [Yarn](https://yarnpkg.com/getting-started/install) (Preferred but not required) 32 | 33 | *A basic understanding of the modern web development workflow. If you don't 34 | know this yet, React might not be for you just yet.* 35 | 36 | ## Getting Started 37 | 38 | First clone the repository or use the template option and place 39 | it within your `resources` folder 40 | 41 | ### Installation 42 | 43 | *The boilerplate was made using `yarn` but is still compatible with 44 | `npm`.* 45 | 46 | Install dependencies by navigating to the `web` folder within 47 | a terminal of your choice and type `npm i` or `yarn`. 48 | 49 | ## Features 50 | 51 | This boilerplate comes with some utilities and examples to work off of. 52 | 53 | ### Lua Utils 54 | 55 | **SendReactMessage** 56 | 57 | This is a small wrapper for dispatching NUI messages. This is designed 58 | to be used with the `useNuiEvent` React hook. 59 | 60 | Signature 61 | ```lua 62 | ---@param action string The action you wish to target 63 | ---@param data any The data you wish to send along with this action 64 | SendReactMessage(action, data) 65 | ``` 66 | 67 | Usage 68 | ```lua 69 | SendReactMessage('setVisible', true) 70 | ``` 71 | 72 | **debugPrint** 73 | 74 | A debug printing utility that is dependent on a convar, 75 | if the convar is set this will print out to the console. 76 | 77 | The convar is dependent on the name given to the resource. 78 | It follows this format `YOUR_RESOURCE_NAME-debugMode` 79 | 80 | To turn on debugMode add `setr YOUR_RESOURCE_NAME-debugMode 1` to 81 | your server.cfg or use the `setr` console command instead. 82 | 83 | Signature (Replicates `print`) 84 | ```lua 85 | ---@param ... any[] The arguments you wish to send 86 | debugPrint(...) 87 | ``` 88 | 89 | Usage 90 | ```lua 91 | debugPrint('wow cool string to print', true, someOtherVar) 92 | ``` 93 | 94 | ### React Utils 95 | 96 | Signatures are not included for these utilities as the type definitions 97 | are sufficient enough. 98 | 99 | **useNuiEvent** 100 | 101 | This is a custom React hook that is designed to intercept and handle 102 | messages dispatched by the game scripts. This is the primary 103 | way of creating passive listeners. 104 | 105 | 106 | *Note: For now handlers can only be registered a single time. I haven't 107 | come across a personal usecase for a cascading event system* 108 | 109 | **Usage** 110 | ```jsx 111 | const MyComp: React.FC = () => { 112 | const [state, setState] = useState('') 113 | 114 | useNuiEvent('myAction', (data) => { 115 | // the first argument to the handler function 116 | // is the data argument sent using SendReactMessage 117 | 118 | // do whatever logic u want here 119 | setState(data) 120 | }) 121 | 122 | return( 123 |
124 |

Some component

125 |

{state}

126 |
127 | ) 128 | } 129 | 130 | ``` 131 | 132 | **fetchNui** 133 | 134 | This is a simple NUI focused wrapper around the standard `fetch` API. 135 | This is the main way to accomplish active NUI data fetching 136 | or to trigger NUI callbacks in the game scripts. 137 | 138 | When using this, you must always at least callback using `{}` 139 | in the gamescripts. 140 | 141 | *This can be heavily customized to your use case* 142 | 143 | **Usage** 144 | ```ts 145 | // First argument is the callback event name. 146 | fetchNui('getClientData').then(retData => { 147 | console.log('Got return data from client scripts:') 148 | console.dir(retData) 149 | setClientData(retData) 150 | }).catch(e => { 151 | console.error('Setting mock data due to error', e) 152 | setClientData({ x: 500, y: 300, z: 200}) 153 | }) 154 | ``` 155 | 156 | **debugData** 157 | 158 | This is a function allowing for mocking dispatched game script 159 | actions in a browser environment. It will trigger `useNuiEvent` handlers 160 | as if they were dispatched by the game scripts. **It will only fire if the current 161 | environment is a regular browser and not CEF** 162 | 163 | **Usage** 164 | ```ts 165 | // This will target the useNuiEvent hooks registered with `setVisible` 166 | // and pass them the data of `true` 167 | debugData([ 168 | { 169 | action: 'setVisible', 170 | data: true, 171 | } 172 | ]) 173 | ``` 174 | 175 | **Misc Utils** 176 | 177 | These are small but useful included utilities. 178 | 179 | * `isEnvBrowser()` - Will return a boolean indicating if the current 180 | environment is a regular browser. (Useful for logic in development) 181 | 182 | ## Development Workflow 183 | 184 | This boilerplate was designed with development workflow in mind. 185 | It includes some helpful scripts to accomplish that. 186 | 187 | **Hot Builds In-Game** 188 | 189 | When developing in-game, you can use the hot build system by 190 | running the `start:game` script. This is essentially the start 191 | script but it writes to disk. Meaning all that is required is a 192 | resource restart to update the game script 193 | 194 | **Usage** 195 | ```sh 196 | # yarn 197 | yarn start:game 198 | # npm 199 | npm run start:game 200 | ``` 201 | 202 | **Production Builds** 203 | 204 | When you are done with development phase for your resource. You 205 | must create a production build that is optimized and minimized. 206 | 207 | You can do this by running the following: 208 | 209 | ```sh 210 | npm run build 211 | yarn build 212 | ``` 213 | 214 | ## Additional Notes 215 | 216 | Need further support? Join our [Discord](https://discord.com/invite/HYwBjTbAY5)! 217 | -------------------------------------------------------------------------------- /client/client.lua: -------------------------------------------------------------------------------- 1 | local function toggleNuiFrame(shouldShow) 2 | SetNuiFocus(shouldShow, shouldShow) 3 | SendReactMessage('setVisible', shouldShow) 4 | end 5 | 6 | RegisterCommand('show-nui', function() 7 | toggleNuiFrame(true) 8 | debugPrint('Show NUI frame') 9 | end) 10 | 11 | RegisterNUICallback('hideFrame', function(_, cb) 12 | toggleNuiFrame(false) 13 | debugPrint('Hide NUI frame') 14 | cb({}) 15 | end) 16 | 17 | RegisterNUICallback('getClientData', function(data, cb) 18 | debugPrint('Data sent by React', json.encode(data)) 19 | 20 | -- Lets send back client coords to the React frame for use 21 | local curCoords = GetEntityCoords(PlayerPedId()) 22 | 23 | local retData = { x = curCoords.x, y = curCoords.y, z = curCoords.z } 24 | cb(retData) 25 | end) -------------------------------------------------------------------------------- /client/utils.lua: -------------------------------------------------------------------------------- 1 | --- A simple wrapper around SendNUIMessage that you can use to 2 | --- dispatch actions to the React frame. 3 | --- 4 | ---@param action string The action you wish to target 5 | ---@param data any The data you wish to send along with this action 6 | function SendReactMessage(action, data) 7 | SendNUIMessage({ 8 | action = action, 9 | data = data 10 | }) 11 | end 12 | 13 | local currentResourceName = GetCurrentResourceName() 14 | 15 | local debugIsEnabled = GetConvarInt(('%s-debugMode'):format(currentResourceName), 0) == 1 16 | 17 | --- A simple debug print function that is dependent on a convar 18 | --- will output a nice prettfied message if debugMode is on 19 | function debugPrint(...) 20 | if not debugIsEnabled then return end 21 | local args = { ... } 22 | 23 | local appendStr = '' 24 | for _, v in ipairs(args) do 25 | appendStr = appendStr .. ' ' .. tostring(v) 26 | end 27 | local msgTemplate = '^3[%s]^0%s' 28 | local finalMsg = msgTemplate:format(currentResourceName, appendStr) 29 | print(finalMsg) 30 | end 31 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version "cerulean" 2 | 3 | description "Basic React (TypeScript) & Lua Game Scripts Boilerplate" 4 | author "Project Error" 5 | version '1.0.0' 6 | repository 'https://github.com/project-error/fivem-react-boilerplate-lua' 7 | 8 | lua54 'yes' 9 | 10 | games { 11 | "gta5", 12 | "rdr3" 13 | } 14 | 15 | ui_page 'web/build/index.html' 16 | 17 | client_script "client/**/*" 18 | server_script "server/**/*" 19 | 20 | files { 21 | 'web/build/index.html', 22 | 'web/build/**/*', 23 | } -------------------------------------------------------------------------------- /server/server.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-error/fivem-react-boilerplate-lua/105f5f75eb01c6c3a0e5b8b79412d0684aef88d3/server/server.lua -------------------------------------------------------------------------------- /web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | build 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NUI React Boilerplate 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "homepage": "web/build", 4 | "private": true, 5 | "type": "module", 6 | "version": "0.1.0", 7 | "scripts": { 8 | "start": "vite", 9 | "start:game": "vite build --watch", 10 | "build": "tsc && vite build", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.37", 19 | "@types/react-dom": "^18.2.15", 20 | "@typescript-eslint/eslint-plugin": "^6.11.0", 21 | "@typescript-eslint/parser": "^6.11.0", 22 | "@vitejs/plugin-react": "^4.2.0", 23 | "eslint": "^8.54.0", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.4", 26 | "typescript": "^5.2.2", 27 | "vite": "^5.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/src/components/App.css: -------------------------------------------------------------------------------- 1 | .nui-wrapper { 2 | text-align: center; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | pre { 10 | counter-reset:line-numbering; 11 | background:#2c3e50; 12 | padding:12px 0px 14px 0; 13 | color:#ecf0f1; 14 | line-height:140%; 15 | } 16 | 17 | .popup-thing { 18 | background: #282c34; 19 | border-radius: 10px; 20 | width: 500px; 21 | height: 400px; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | color: white; 26 | } 27 | -------------------------------------------------------------------------------- /web/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./App.css"; 3 | import { debugData } from "../utils/debugData"; 4 | import { fetchNui } from "../utils/fetchNui"; 5 | 6 | // This will set the NUI to visible if we are 7 | // developing in browser 8 | debugData([ 9 | { 10 | action: "setVisible", 11 | data: true, 12 | }, 13 | ]); 14 | 15 | interface ReturnClientDataCompProps { 16 | data: unknown; 17 | } 18 | 19 | const ReturnClientDataComp: React.FC = ({ 20 | data, 21 | }) => ( 22 | <> 23 |
Returned Data:
24 |
25 |       {JSON.stringify(data, null)}
26 |     
27 | 28 | ); 29 | 30 | interface ReturnData { 31 | x: number; 32 | y: number; 33 | z: number; 34 | } 35 | 36 | const App: React.FC = () => { 37 | const [clientData, setClientData] = useState(null); 38 | 39 | const handleGetClientData = () => { 40 | fetchNui("getClientData") 41 | .then((retData) => { 42 | console.log("Got return data from client scripts:"); 43 | console.dir(retData); 44 | setClientData(retData); 45 | }) 46 | .catch((e) => { 47 | console.error("Setting mock data due to error", e); 48 | setClientData({ x: 500, y: 300, z: 200 }); 49 | }); 50 | }; 51 | 52 | return ( 53 |
54 |
55 |
56 |

This is the NUI Popup!

57 |

Exit with the escape key

58 | 59 | {clientData && } 60 |
61 |
62 |
63 | ); 64 | }; 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /web/src/hooks/useNuiEvent.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef } from "react"; 2 | import { noop } from "../utils/misc"; 3 | 4 | interface NuiMessageData { 5 | action: string; 6 | data: T; 7 | } 8 | 9 | type NuiHandlerSignature = (data: T) => void; 10 | 11 | /** 12 | * A hook that manage events listeners for receiving data from the client scripts 13 | * @param action The specific `action` that should be listened for. 14 | * @param handler The callback function that will handle data relayed by this hook 15 | * 16 | * @example 17 | * useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => { 18 | * // whatever logic you want 19 | * }) 20 | * 21 | **/ 22 | 23 | export const useNuiEvent = ( 24 | action: string, 25 | handler: (data: T) => void, 26 | ) => { 27 | const savedHandler: MutableRefObject> = useRef(noop); 28 | 29 | // Make sure we handle for a reactive handler 30 | useEffect(() => { 31 | savedHandler.current = handler; 32 | }, [handler]); 33 | 34 | useEffect(() => { 35 | const eventListener = (event: MessageEvent>) => { 36 | const { action: eventAction, data } = event.data; 37 | 38 | if (savedHandler.current) { 39 | if (eventAction === action) { 40 | savedHandler.current(data); 41 | } 42 | } 43 | }; 44 | 45 | window.addEventListener("message", eventListener); 46 | // Remove Event Listener on component cleanup 47 | return () => window.removeEventListener("message", eventListener); 48 | }, [action]); 49 | }; 50 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | height: 100vh; 9 | } 10 | 11 | #root { 12 | height: 100% 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { VisibilityProvider } from './providers/VisibilityProvider'; 4 | import App from './components/App'; 5 | import './index.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /web/src/providers/VisibilityProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Context, 3 | createContext, 4 | useContext, 5 | useEffect, 6 | useState, 7 | } from "react"; 8 | import { useNuiEvent } from "../hooks/useNuiEvent"; 9 | import { fetchNui } from "../utils/fetchNui"; 10 | import { isEnvBrowser } from "../utils/misc"; 11 | 12 | const VisibilityCtx = createContext(null); 13 | 14 | interface VisibilityProviderValue { 15 | setVisible: (visible: boolean) => void; 16 | visible: boolean; 17 | } 18 | 19 | // This should be mounted at the top level of your application, it is currently set to 20 | // apply a CSS visibility value. If this is non-performant, this should be customized. 21 | export const VisibilityProvider: React.FC<{ children: React.ReactNode }> = ({ 22 | children, 23 | }) => { 24 | const [visible, setVisible] = useState(false); 25 | 26 | useNuiEvent("setVisible", setVisible); 27 | 28 | // Handle pressing escape/backspace 29 | useEffect(() => { 30 | // Only attach listener when we are visible 31 | if (!visible) return; 32 | 33 | const keyHandler = (e: KeyboardEvent) => { 34 | if (["Backspace", "Escape"].includes(e.code)) { 35 | if (!isEnvBrowser()) fetchNui("hideFrame"); 36 | else setVisible(!visible); 37 | } 38 | }; 39 | 40 | window.addEventListener("keydown", keyHandler); 41 | 42 | return () => window.removeEventListener("keydown", keyHandler); 43 | }, [visible]); 44 | 45 | return ( 46 | 52 |
55 | {children} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export const useVisibility = () => 62 | useContext( 63 | VisibilityCtx as Context, 64 | ); 65 | -------------------------------------------------------------------------------- /web/src/utils/debugData.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser } from "./misc"; 2 | 3 | interface DebugEvent { 4 | action: string; 5 | data: T; 6 | } 7 | 8 | /** 9 | * Emulates dispatching an event using SendNuiMessage in the lua scripts. 10 | * This is used when developing in browser 11 | * 12 | * @param events - The event you want to cover 13 | * @param timer - How long until it should trigger (ms) 14 | */ 15 | export const debugData =

(events: DebugEvent

[], timer = 1000): void => { 16 | if (import.meta.env.MODE === "development" && isEnvBrowser()) { 17 | for (const event of events) { 18 | setTimeout(() => { 19 | window.dispatchEvent( 20 | new MessageEvent("message", { 21 | data: { 22 | action: event.action, 23 | data: event.data, 24 | }, 25 | }), 26 | ); 27 | }, timer); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /web/src/utils/fetchNui.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser } from "./misc"; 2 | 3 | /** 4 | * Simple wrapper around fetch API tailored for CEF/NUI use. This abstraction 5 | * can be extended to include AbortController if needed or if the response isn't 6 | * JSON. Tailor it to your needs. 7 | * 8 | * @param eventName - The endpoint eventname to target 9 | * @param data - Data you wish to send in the NUI Callback 10 | * @param mockData - Mock data to be returned if in the browser 11 | * 12 | * @return returnData - A promise for the data sent back by the NuiCallbacks CB argument 13 | */ 14 | 15 | export async function fetchNui( 16 | eventName: string, 17 | data?: unknown, 18 | mockData?: T, 19 | ): Promise { 20 | const options = { 21 | method: "post", 22 | headers: { 23 | "Content-Type": "application/json; charset=UTF-8", 24 | }, 25 | body: JSON.stringify(data), 26 | }; 27 | 28 | if (isEnvBrowser() && mockData) return mockData; 29 | 30 | const resourceName = (window as any).GetParentResourceName 31 | ? (window as any).GetParentResourceName() 32 | : "nui-frame-app"; 33 | 34 | const resp = await fetch(`https://${resourceName}/${eventName}`, options); 35 | 36 | const respFormatted = await resp.json(); 37 | 38 | return respFormatted; 39 | } 40 | -------------------------------------------------------------------------------- /web/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | // Will return whether the current environment is in a regular browser 2 | // and not CEF 3 | export const isEnvBrowser = (): boolean => !(window as any).invokeNative; 4 | 5 | // Basic no operation function 6 | export const noop = () => {}; 7 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: './', 8 | build: { 9 | outDir: 'build', 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------