@zidanpro/create-nui-fivem 3.0.2 ā 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +44 -44
- package/package.json +28 -21
- package/template/README.md +51 -51
- package/template/eslint.config.js +22 -22
- package/template/index.html +12 -12
- package/template/package.json +29 -29
- package/template/src/App.css +49 -49
- package/template/src/App.tsx +36 -36
- package/template/src/hooks/useExitListener.ts +30 -30
- package/template/src/hooks/useNuiEvent.ts +30 -30
- package/template/src/index.css +20 -20
- package/template/src/main.tsx +21 -21
- package/template/src/utils/fetchNui.ts +24 -24
- package/template/src/utils/misc.ts +1 -1
- package/template/tsconfig.app.json +26 -26
- package/template/tsconfig.json +6 -6
- package/template/tsconfig.node.json +24 -24
- package/template/vite.config.ts +11 -11
package/bin/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const ora = require("ora").default;
|
|
6
|
-
|
|
7
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
8
|
-
|
|
9
|
-
const projectName = process.argv[2];
|
|
10
|
-
|
|
11
|
-
const targetDir = projectName
|
|
12
|
-
? path.resolve(process.cwd(), projectName)
|
|
13
|
-
: process.cwd();
|
|
14
|
-
|
|
15
|
-
const templateDir = path.join(__dirname, "../template");
|
|
16
|
-
|
|
17
|
-
(async () => {
|
|
18
|
-
const spinner = ora("š Starting project creation...").start();
|
|
19
|
-
|
|
20
|
-
await sleep(800);
|
|
21
|
-
|
|
22
|
-
spinner.text = "š Preparing folder...";
|
|
23
|
-
await sleep(800);
|
|
24
|
-
|
|
25
|
-
if (projectName && !fs.existsSync(targetDir)) {
|
|
26
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
spinner.text = "š¦ Copying template files...";
|
|
30
|
-
await sleep(1200);
|
|
31
|
-
|
|
32
|
-
fs.cpSync(templateDir, targetDir, { recursive: true });
|
|
33
|
-
|
|
34
|
-
spinner.text = "⨠Finalizing setup...";
|
|
35
|
-
await sleep(700);
|
|
36
|
-
|
|
37
|
-
spinner.succeed("ā
Project created successfully!");
|
|
38
|
-
|
|
39
|
-
await sleep(300);
|
|
40
|
-
|
|
41
|
-
console.log("\nš Next steps:");
|
|
42
|
-
console.log(` cd ${projectName || "."}`);
|
|
43
|
-
console.log(" npm install");
|
|
44
|
-
console.log(" npm run dev\n");
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const ora = require("ora").default;
|
|
6
|
+
|
|
7
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
8
|
+
|
|
9
|
+
const projectName = process.argv[2];
|
|
10
|
+
|
|
11
|
+
const targetDir = projectName
|
|
12
|
+
? path.resolve(process.cwd(), projectName)
|
|
13
|
+
: process.cwd();
|
|
14
|
+
|
|
15
|
+
const templateDir = path.join(__dirname, "../template");
|
|
16
|
+
|
|
17
|
+
(async () => {
|
|
18
|
+
const spinner = ora("š Starting project creation...").start();
|
|
19
|
+
|
|
20
|
+
await sleep(800);
|
|
21
|
+
|
|
22
|
+
spinner.text = "š Preparing folder...";
|
|
23
|
+
await sleep(800);
|
|
24
|
+
|
|
25
|
+
if (projectName && !fs.existsSync(targetDir)) {
|
|
26
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.text = "š¦ Copying template files...";
|
|
30
|
+
await sleep(1200);
|
|
31
|
+
|
|
32
|
+
fs.cpSync(templateDir, targetDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
spinner.text = "⨠Finalizing setup...";
|
|
35
|
+
await sleep(700);
|
|
36
|
+
|
|
37
|
+
spinner.succeed("ā
Project created successfully!");
|
|
38
|
+
|
|
39
|
+
await sleep(300);
|
|
40
|
+
|
|
41
|
+
console.log("\nš Next steps:");
|
|
42
|
+
console.log(` cd ${projectName || "."}`);
|
|
43
|
+
console.log(" npm install");
|
|
44
|
+
console.log(" npm run dev\n");
|
|
45
45
|
})();
|
package/package.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zidanpro/create-nui-fivem",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "FiveM React Boilerplate Generator",
|
|
5
|
-
"bin": {
|
|
6
|
-
"create-nui-fivem": "./bin/index.js"
|
|
7
|
-
},
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@zidanpro/create-nui-fivem",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "FiveM React Boilerplate Generator",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-nui-fivem": "./bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/ZidanPro/create-nui-fivem.git"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"template"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"fivem",
|
|
21
|
+
"react",
|
|
22
|
+
"vite"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ora": "^9.4.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/template/README.md
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
# gusti-nui-boilerplate
|
|
2
|
-
|
|
3
|
-
A boilerplate for building Gusti NUI applications
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install
|
|
9
|
-
#or
|
|
10
|
-
yarn install
|
|
11
|
-
#or
|
|
12
|
-
pnpm install
|
|
13
|
-
#or
|
|
14
|
-
bun install
|
|
15
|
-
#or
|
|
16
|
-
deno install
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Client Side
|
|
20
|
-
`client.lua`
|
|
21
|
-
|
|
22
|
-
```lua
|
|
23
|
-
RegisterCommand("nui", function()
|
|
24
|
-
SetNuiFocus(true, true)
|
|
25
|
-
SendNUIMessage({
|
|
26
|
-
action = "setVisible",
|
|
27
|
-
data = { visible = true }
|
|
28
|
-
})
|
|
29
|
-
end)
|
|
30
|
-
|
|
31
|
-
RegisterNUICallback("exit", function(data, cb)
|
|
32
|
-
SetNuiFocus(false, false)
|
|
33
|
-
SendNUIMessage({
|
|
34
|
-
action = "setVisible",
|
|
35
|
-
data = { visible = false }
|
|
36
|
-
})
|
|
37
|
-
cb("ok")
|
|
38
|
-
end)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## FxManifest
|
|
42
|
-
`fxmanifest.lua`
|
|
43
|
-
|
|
44
|
-
```lua
|
|
45
|
-
ui_page 'web/build/index.html'
|
|
46
|
-
|
|
47
|
-
files {
|
|
48
|
-
'web/build/index.html',
|
|
49
|
-
'web/build/assets/*.js',
|
|
50
|
-
'web/build/assets/*.css',
|
|
51
|
-
}
|
|
1
|
+
# gusti-nui-boilerplate
|
|
2
|
+
|
|
3
|
+
A boilerplate for building Gusti NUI applications
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
#or
|
|
10
|
+
yarn install
|
|
11
|
+
#or
|
|
12
|
+
pnpm install
|
|
13
|
+
#or
|
|
14
|
+
bun install
|
|
15
|
+
#or
|
|
16
|
+
deno install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Client Side
|
|
20
|
+
`client.lua`
|
|
21
|
+
|
|
22
|
+
```lua
|
|
23
|
+
RegisterCommand("nui", function()
|
|
24
|
+
SetNuiFocus(true, true)
|
|
25
|
+
SendNUIMessage({
|
|
26
|
+
action = "setVisible",
|
|
27
|
+
data = { visible = true }
|
|
28
|
+
})
|
|
29
|
+
end)
|
|
30
|
+
|
|
31
|
+
RegisterNUICallback("exit", function(data, cb)
|
|
32
|
+
SetNuiFocus(false, false)
|
|
33
|
+
SendNUIMessage({
|
|
34
|
+
action = "setVisible",
|
|
35
|
+
data = { visible = false }
|
|
36
|
+
})
|
|
37
|
+
cb("ok")
|
|
38
|
+
end)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## FxManifest
|
|
42
|
+
`fxmanifest.lua`
|
|
43
|
+
|
|
44
|
+
```lua
|
|
45
|
+
ui_page 'web/build/index.html'
|
|
46
|
+
|
|
47
|
+
files {
|
|
48
|
+
'web/build/index.html',
|
|
49
|
+
'web/build/assets/*.js',
|
|
50
|
+
'web/build/assets/*.css',
|
|
51
|
+
}
|
|
52
52
|
```
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import js from '@eslint/js'
|
|
2
|
-
import globals from 'globals'
|
|
3
|
-
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
-
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
-
import tseslint from 'typescript-eslint'
|
|
6
|
-
import { globalIgnores } from 'eslint/config'
|
|
7
|
-
|
|
8
|
-
export default tseslint.config([
|
|
9
|
-
globalIgnores(['dist']),
|
|
10
|
-
{
|
|
11
|
-
files: ['**/*.{ts,tsx}'],
|
|
12
|
-
extends: [
|
|
13
|
-
js.configs.recommended,
|
|
14
|
-
tseslint.configs.recommended,
|
|
15
|
-
reactHooks.configs['recommended-latest'],
|
|
16
|
-
reactRefresh.configs.vite,
|
|
17
|
-
],
|
|
18
|
-
languageOptions: {
|
|
19
|
-
ecmaVersion: 2020,
|
|
20
|
-
globals: globals.browser,
|
|
21
|
-
},
|
|
22
|
-
}
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default tseslint.config([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs['recommended-latest'],
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
23
|
])
|
package/template/index.html
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
-
<title>Gusti NUI Boilerplate</title>
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<div id="root"></div>
|
|
11
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
-
</body>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Gusti NUI Boilerplate</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
13
|
</html>
|
package/template/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gusti-nui-boilerplate",
|
|
3
|
-
"private": true,
|
|
4
|
-
"description": "A boilerplate for building Gusti NUI applications",
|
|
5
|
-
"version": "0.0.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"dev": "vite",
|
|
9
|
-
"build": "tsc -b && vite build",
|
|
10
|
-
"lint": "eslint .",
|
|
11
|
-
"preview": "vite preview"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"react": "^19.1.0",
|
|
15
|
-
"react-dom": "^19.1.0"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@eslint/js": "^9.30.1",
|
|
19
|
-
"@types/react": "^19.1.8",
|
|
20
|
-
"@types/react-dom": "^19.1.6",
|
|
21
|
-
"@vitejs/plugin-react": "^4.6.0",
|
|
22
|
-
"eslint": "^9.30.1",
|
|
23
|
-
"eslint-plugin-react-hooks": "^5.2.0",
|
|
24
|
-
"eslint-plugin-react-refresh": "^0.4.20",
|
|
25
|
-
"globals": "^16.3.0",
|
|
26
|
-
"typescript": "~5.8.3",
|
|
27
|
-
"typescript-eslint": "^8.35.1",
|
|
28
|
-
"vite": "^7.0.4"
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gusti-nui-boilerplate",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "A boilerplate for building Gusti NUI applications",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc -b && vite build",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"preview": "vite preview"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"react": "^19.1.0",
|
|
15
|
+
"react-dom": "^19.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@eslint/js": "^9.30.1",
|
|
19
|
+
"@types/react": "^19.1.8",
|
|
20
|
+
"@types/react-dom": "^19.1.6",
|
|
21
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
22
|
+
"eslint": "^9.30.1",
|
|
23
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
24
|
+
"eslint-plugin-react-refresh": "^0.4.20",
|
|
25
|
+
"globals": "^16.3.0",
|
|
26
|
+
"typescript": "~5.8.3",
|
|
27
|
+
"typescript-eslint": "^8.35.1",
|
|
28
|
+
"vite": "^7.0.4"
|
|
29
|
+
}
|
|
30
30
|
}
|
package/template/src/App.css
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
/* Optional */
|
|
3
|
-
-webkit-scrollbar: none;
|
|
4
|
-
scrollbar-width: none;
|
|
5
|
-
-ms-overflow-style: none;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.wrapper {
|
|
9
|
-
position: fixed;
|
|
10
|
-
inset: 0;
|
|
11
|
-
display: flex;
|
|
12
|
-
justify-content: center;
|
|
13
|
-
align-items: center;
|
|
14
|
-
background-color: rgba(0, 0, 0, 0.5);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.container {
|
|
18
|
-
background-color: #fff;
|
|
19
|
-
border-radius: 1rem;
|
|
20
|
-
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
21
|
-
padding: 2rem;
|
|
22
|
-
width: 100%;
|
|
23
|
-
max-width: 420px;
|
|
24
|
-
text-align: center;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.title {
|
|
28
|
-
font-size: 1.5rem;
|
|
29
|
-
font-weight: bold;
|
|
30
|
-
margin-bottom: 1rem;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.description {
|
|
34
|
-
color: #4b5563;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.btn-primary {
|
|
38
|
-
margin-top: 1rem;
|
|
39
|
-
background-color: #3b82f6;
|
|
40
|
-
color: white;
|
|
41
|
-
padding: 0.5rem 1rem;
|
|
42
|
-
border-radius: 0.375rem;
|
|
43
|
-
border: none;
|
|
44
|
-
cursor: pointer;
|
|
45
|
-
transition: background-color 0.2s ease;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.btn-primary:hover {
|
|
49
|
-
background-color: #2563eb;
|
|
1
|
+
:root {
|
|
2
|
+
/* Optional */
|
|
3
|
+
-webkit-scrollbar: none;
|
|
4
|
+
scrollbar-width: none;
|
|
5
|
+
-ms-overflow-style: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.wrapper {
|
|
9
|
+
position: fixed;
|
|
10
|
+
inset: 0;
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.container {
|
|
18
|
+
background-color: #fff;
|
|
19
|
+
border-radius: 1rem;
|
|
20
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
21
|
+
padding: 2rem;
|
|
22
|
+
width: 100%;
|
|
23
|
+
max-width: 420px;
|
|
24
|
+
text-align: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.title {
|
|
28
|
+
font-size: 1.5rem;
|
|
29
|
+
font-weight: bold;
|
|
30
|
+
margin-bottom: 1rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.description {
|
|
34
|
+
color: #4b5563;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.btn-primary {
|
|
38
|
+
margin-top: 1rem;
|
|
39
|
+
background-color: #3b82f6;
|
|
40
|
+
color: white;
|
|
41
|
+
padding: 0.5rem 1rem;
|
|
42
|
+
border-radius: 0.375rem;
|
|
43
|
+
border: none;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
transition: background-color 0.2s ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.btn-primary:hover {
|
|
49
|
+
background-color: #2563eb;
|
|
50
50
|
}
|
package/template/src/App.tsx
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import './App.css'
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { isEnvBrowser } from './utils/misc';
|
|
4
|
-
import { useNuiEvent } from './hooks/useNuiEvent';
|
|
5
|
-
import { useExitListener } from './hooks/useExitListener';
|
|
6
|
-
import { fetchNui } from './utils/fetchNui';
|
|
7
|
-
|
|
8
|
-
export default function App() {
|
|
9
|
-
const [visible, setVisible] = useState(isEnvBrowser());
|
|
10
|
-
|
|
11
|
-
useNuiEvent('setVisible', (data: { visible?: boolean }) => {
|
|
12
|
-
setVisible(data.visible || false);
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
useExitListener(setVisible);
|
|
16
|
-
|
|
17
|
-
const handleClose = () => {
|
|
18
|
-
setVisible(false);
|
|
19
|
-
void fetchNui('exit');
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<>
|
|
24
|
-
{visible && (
|
|
25
|
-
<div className="wrapper">
|
|
26
|
-
<div className="container">
|
|
27
|
-
<h1 className="title">š„Hello, World!</h1>
|
|
28
|
-
<p className="description">Welcome to the Gusti NUI Boilerplate.</p>
|
|
29
|
-
<button onClick={handleClose} className="btn-primary">
|
|
30
|
-
Get Started
|
|
31
|
-
</button>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
)}
|
|
35
|
-
</>
|
|
36
|
-
);
|
|
1
|
+
import './App.css'
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { isEnvBrowser } from './utils/misc';
|
|
4
|
+
import { useNuiEvent } from './hooks/useNuiEvent';
|
|
5
|
+
import { useExitListener } from './hooks/useExitListener';
|
|
6
|
+
import { fetchNui } from './utils/fetchNui';
|
|
7
|
+
|
|
8
|
+
export default function App() {
|
|
9
|
+
const [visible, setVisible] = useState(isEnvBrowser());
|
|
10
|
+
|
|
11
|
+
useNuiEvent('setVisible', (data: { visible?: boolean }) => {
|
|
12
|
+
setVisible(data.visible || false);
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
useExitListener(setVisible);
|
|
16
|
+
|
|
17
|
+
const handleClose = () => {
|
|
18
|
+
setVisible(false);
|
|
19
|
+
void fetchNui('exit');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{visible && (
|
|
25
|
+
<div className="wrapper">
|
|
26
|
+
<div className="container">
|
|
27
|
+
<h1 className="title">š„Hello, World!</h1>
|
|
28
|
+
<p className="description">Welcome to the Gusti NUI Boilerplate.</p>
|
|
29
|
+
<button onClick={handleClose} className="btn-primary">
|
|
30
|
+
Get Started
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
37
|
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { noop } from '../utils/misc';
|
|
3
|
-
import { fetchNui } from '../utils/fetchNui';
|
|
4
|
-
|
|
5
|
-
type FrameVisibleSetter = (bool: boolean) => void;
|
|
6
|
-
|
|
7
|
-
const LISTENED_KEYS = ['Escape'];
|
|
8
|
-
|
|
9
|
-
export const useExitListener = (visibleSetter: FrameVisibleSetter) => {
|
|
10
|
-
const setterRef = useRef<FrameVisibleSetter>(noop);
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
setterRef.current = visibleSetter;
|
|
14
|
-
}, [visibleSetter]);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
18
|
-
if (LISTENED_KEYS.includes(event.key)) {
|
|
19
|
-
event.preventDefault();
|
|
20
|
-
setterRef.current(false);
|
|
21
|
-
fetchNui('exit');
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
26
|
-
|
|
27
|
-
return () => {
|
|
28
|
-
window.removeEventListener('keydown', handleKeyDown);
|
|
29
|
-
};
|
|
30
|
-
}, []);
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { noop } from '../utils/misc';
|
|
3
|
+
import { fetchNui } from '../utils/fetchNui';
|
|
4
|
+
|
|
5
|
+
type FrameVisibleSetter = (bool: boolean) => void;
|
|
6
|
+
|
|
7
|
+
const LISTENED_KEYS = ['Escape'];
|
|
8
|
+
|
|
9
|
+
export const useExitListener = (visibleSetter: FrameVisibleSetter) => {
|
|
10
|
+
const setterRef = useRef<FrameVisibleSetter>(noop);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setterRef.current = visibleSetter;
|
|
14
|
+
}, [visibleSetter]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
18
|
+
if (LISTENED_KEYS.includes(event.key)) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
setterRef.current(false);
|
|
21
|
+
fetchNui('exit');
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
31
|
};
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { noop } from '../utils/misc';
|
|
3
|
-
|
|
4
|
-
interface NuiMessageData<T = unknown> {
|
|
5
|
-
action: string;
|
|
6
|
-
data: T;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type NuiHandlerSignature<T> = (data: T) => void;
|
|
10
|
-
|
|
11
|
-
export const useNuiEvent = <T = unknown>(action: string, handler: (data: T) => void) => {
|
|
12
|
-
const handlerRef = useRef<NuiHandlerSignature<T>>(noop);
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
handlerRef.current = handler;
|
|
16
|
-
}, [handler]);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
const handleNuiMessage = (event: MessageEvent<NuiMessageData<T>>) => {
|
|
20
|
-
if (event.data.action === action) {
|
|
21
|
-
handlerRef.current(event.data.data);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
window.addEventListener('message', handleNuiMessage as EventListener);
|
|
26
|
-
|
|
27
|
-
return () => {
|
|
28
|
-
window.removeEventListener('message', handleNuiMessage as EventListener);
|
|
29
|
-
};
|
|
30
|
-
}, [action]);
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { noop } from '../utils/misc';
|
|
3
|
+
|
|
4
|
+
interface NuiMessageData<T = unknown> {
|
|
5
|
+
action: string;
|
|
6
|
+
data: T;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type NuiHandlerSignature<T> = (data: T) => void;
|
|
10
|
+
|
|
11
|
+
export const useNuiEvent = <T = unknown>(action: string, handler: (data: T) => void) => {
|
|
12
|
+
const handlerRef = useRef<NuiHandlerSignature<T>>(noop);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
handlerRef.current = handler;
|
|
16
|
+
}, [handler]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handleNuiMessage = (event: MessageEvent<NuiMessageData<T>>) => {
|
|
20
|
+
if (event.data.action === action) {
|
|
21
|
+
handlerRef.current(event.data.data);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
window.addEventListener('message', handleNuiMessage as EventListener);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('message', handleNuiMessage as EventListener);
|
|
29
|
+
};
|
|
30
|
+
}, [action]);
|
|
31
31
|
};
|
package/template/src/index.css
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
#root {
|
|
2
|
-
height: 100%;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
:root {
|
|
6
|
-
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
7
|
-
background-color: none !important;
|
|
8
|
-
font-synthesis: none;
|
|
9
|
-
text-rendering: optimizeLegibility;
|
|
10
|
-
-webkit-font-smoothing: antialiased;
|
|
11
|
-
-moz-osx-font-smoothing: grayscale;
|
|
12
|
-
/* Optional */
|
|
13
|
-
-webkit-scrollbar: none;
|
|
14
|
-
scrollbar-width: none;
|
|
15
|
-
-ms-overflow-style: none;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
body {
|
|
19
|
-
margin: 0;
|
|
20
|
-
height: 100vh;
|
|
1
|
+
#root {
|
|
2
|
+
height: 100%;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
7
|
+
background-color: none !important;
|
|
8
|
+
font-synthesis: none;
|
|
9
|
+
text-rendering: optimizeLegibility;
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
/* Optional */
|
|
13
|
+
-webkit-scrollbar: none;
|
|
14
|
+
scrollbar-width: none;
|
|
15
|
+
-ms-overflow-style: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
height: 100vh;
|
|
21
21
|
}
|
package/template/src/main.tsx
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { StrictMode } from 'react'
|
|
2
|
-
import { createRoot } from 'react-dom/client'
|
|
3
|
-
import './index.css'
|
|
4
|
-
import App from './App.tsx'
|
|
5
|
-
import { isEnvBrowser } from './utils/misc.ts';
|
|
6
|
-
|
|
7
|
-
if (isEnvBrowser()) {
|
|
8
|
-
const root = document.getElementById('root');
|
|
9
|
-
|
|
10
|
-
// https://i.imgur.com/iPTAdYV.png - Night time img
|
|
11
|
-
// https://i.imgur.com/3pzRj9n.png - Day time img
|
|
12
|
-
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
|
|
13
|
-
root!.style.backgroundSize = 'cover';
|
|
14
|
-
root!.style.backgroundRepeat = 'no-repeat';
|
|
15
|
-
root!.style.backgroundPosition = 'center';
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
createRoot(document.getElementById('root')!).render(
|
|
19
|
-
<StrictMode>
|
|
20
|
-
<App />
|
|
21
|
-
</StrictMode>
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import App from './App.tsx'
|
|
5
|
+
import { isEnvBrowser } from './utils/misc.ts';
|
|
6
|
+
|
|
7
|
+
if (isEnvBrowser()) {
|
|
8
|
+
const root = document.getElementById('root');
|
|
9
|
+
|
|
10
|
+
// https://i.imgur.com/iPTAdYV.png - Night time img
|
|
11
|
+
// https://i.imgur.com/3pzRj9n.png - Day time img
|
|
12
|
+
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
|
|
13
|
+
root!.style.backgroundSize = 'cover';
|
|
14
|
+
root!.style.backgroundRepeat = 'no-repeat';
|
|
15
|
+
root!.style.backgroundPosition = 'center';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
createRoot(document.getElementById('root')!).render(
|
|
19
|
+
<StrictMode>
|
|
20
|
+
<App />
|
|
21
|
+
</StrictMode>
|
|
22
22
|
)
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { isEnvBrowser } from './misc';
|
|
2
|
-
|
|
3
|
-
export async function fetchNui<T = unknown>(
|
|
4
|
-
eventName: string,
|
|
5
|
-
data?: unknown,
|
|
6
|
-
mock?: { data: T; delay?: number },
|
|
7
|
-
): Promise<T> {
|
|
8
|
-
if (isEnvBrowser()) {
|
|
9
|
-
if (!mock) return await new Promise((resolve) => resolve);
|
|
10
|
-
await new Promise((resolve) => setTimeout(resolve, mock.delay));
|
|
11
|
-
return mock.data;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const options = {
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: {
|
|
17
|
-
'Content-Type': 'application/json; charset=UTF-8',
|
|
18
|
-
},
|
|
19
|
-
body: JSON.stringify(data),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const resourceName = (window as any).GetParentResourceName ? (window as any).GetParentResourceName() : 'gusti-nui-boilerplate';
|
|
23
|
-
const response = await fetch(`https://${resourceName}/${eventName}`, options);
|
|
24
|
-
return (await response.json()) as T;
|
|
1
|
+
import { isEnvBrowser } from './misc';
|
|
2
|
+
|
|
3
|
+
export async function fetchNui<T = unknown>(
|
|
4
|
+
eventName: string,
|
|
5
|
+
data?: unknown,
|
|
6
|
+
mock?: { data: T; delay?: number },
|
|
7
|
+
): Promise<T> {
|
|
8
|
+
if (isEnvBrowser()) {
|
|
9
|
+
if (!mock) return await new Promise((resolve) => resolve);
|
|
10
|
+
await new Promise((resolve) => setTimeout(resolve, mock.delay));
|
|
11
|
+
return mock.data;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const options = {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify(data),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resourceName = (window as any).GetParentResourceName ? (window as any).GetParentResourceName() : 'gusti-nui-boilerplate';
|
|
23
|
+
const response = await fetch(`https://${resourceName}/${eventName}`, options);
|
|
24
|
+
return (await response.json()) as T;
|
|
25
25
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
|
1
|
+
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
|
2
2
|
export const noop = () => {};
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"useDefineForClassFields": true,
|
|
6
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
-
"module": "ESNext",
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
|
|
10
|
-
/* Bundler mode */
|
|
11
|
-
"moduleResolution": "bundler",
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"verbatimModuleSyntax": true,
|
|
14
|
-
"moduleDetection": "force",
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
"jsx": "react-jsx",
|
|
17
|
-
|
|
18
|
-
/* Linting */
|
|
19
|
-
"strict": true,
|
|
20
|
-
"noUnusedLocals": true,
|
|
21
|
-
"noUnusedParameters": true,
|
|
22
|
-
"erasableSyntaxOnly": true,
|
|
23
|
-
"noFallthroughCasesInSwitch": true,
|
|
24
|
-
"noUncheckedSideEffectImports": true
|
|
25
|
-
},
|
|
26
|
-
"include": ["src"]
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"erasableSyntaxOnly": true,
|
|
23
|
+
"noFallthroughCasesInSwitch": true,
|
|
24
|
+
"noUncheckedSideEffectImports": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["src"]
|
|
27
27
|
}
|
package/template/tsconfig.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
"files": [],
|
|
3
|
-
"references": [
|
|
4
|
-
{ "path": "./tsconfig.app.json" },
|
|
5
|
-
{ "path": "./tsconfig.node.json" }
|
|
6
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"files": [],
|
|
3
|
+
"references": [
|
|
4
|
+
{ "path": "./tsconfig.app.json" },
|
|
5
|
+
{ "path": "./tsconfig.node.json" }
|
|
6
|
+
]
|
|
7
7
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
-
"target": "ES2023",
|
|
5
|
-
"lib": ["ES2023"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
|
|
9
|
-
/* Bundler mode */
|
|
10
|
-
"moduleResolution": "Bundler",
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"verbatimModuleSyntax": true,
|
|
13
|
-
"moduleDetection": "force",
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
|
|
16
|
-
/* Linting */
|
|
17
|
-
"strict": true,
|
|
18
|
-
"noUnusedLocals": true,
|
|
19
|
-
"noUnusedParameters": true,
|
|
20
|
-
"erasableSyntaxOnly": true,
|
|
21
|
-
"noFallthroughCasesInSwitch": true,
|
|
22
|
-
"noUncheckedSideEffectImports": true
|
|
23
|
-
},
|
|
24
|
-
"include": ["vite.config.ts"]
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["vite.config.ts"]
|
|
25
25
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
import react from '@vitejs/plugin-react'
|
|
3
|
-
|
|
4
|
-
// https://vite.dev/config/
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
plugins: [react()],
|
|
7
|
-
base: './',
|
|
8
|
-
publicDir: false,
|
|
9
|
-
build: {
|
|
10
|
-
outDir: 'build',
|
|
11
|
-
},
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
// https://vite.dev/config/
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
base: './',
|
|
8
|
+
publicDir: false,
|
|
9
|
+
build: {
|
|
10
|
+
outDir: 'build',
|
|
11
|
+
},
|
|
12
12
|
})
|