all done
This commit is contained in:
parent
f16a32b63e
commit
1259e726bb
220
README.md
220
README.md
@ -1,216 +1,24 @@
|
||||
<div align="center">
|
||||
<img href="https://projecterror.dev" width="150" src="https://i.tasoagc.dev/c1pD" alt="Material-UI logo" />
|
||||
</div>
|
||||
<h1 align="center">FiveM React and Lua Boilerplate</h1>
|
||||
# Sticks Keypad
|
||||
|
||||
<div align="center">
|
||||
A simple and extendable React (TypeScript) boilerplate designed around the Lua ScRT
|
||||
</div>
|
||||
A simple script to have a keypad inside of your city.
|
||||
|
||||
<div align="center">
|
||||
## Installation
|
||||
|
||||
[](https://github.com/project-error/pe-utils/master/LICENSE)
|
||||

|
||||

|
||||
[](https://dependabot.com)
|
||||
</div>
|
||||
1. Download the latest version of the script using the "Download ZIP" button on the right side of this page.
|
||||
2. Extract the ZIP file.
|
||||
3. Rename the folder to `sticks-keypad` and copy it to your `resources` directory.
|
||||
4. Add `ensure sticks-keypad` to your `server.cfg`.
|
||||
5. All done, start your server and enjoy!
|
||||
|
||||
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.
|
||||
## Configuration
|
||||
|
||||
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
|
||||
You can configure the script in the `server/server.lua` file. To add a new entry, you need the door ID, the cords of the door, the teleport cords, and the password. An example is already in the file to help you understand how to add a new entry, and can also be seen below.
|
||||
|
||||
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)
|
||||
-- Example
|
||||
{1, {x = -3029.3825683594, y = 72.813552856445, z = 11.4}, {x = -3031.3232421875, y = 93.021644592285, z = 12.346099853516}, "1234"},
|
||||
```
|
||||
|
||||
Usage
|
||||
```lua
|
||||
SendReactMessage('setVisible', true)
|
||||
```
|
||||
## Credits
|
||||
|
||||
**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<string>('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(
|
||||
<div>
|
||||
<h1>Some component</h1>
|
||||
<p>{state}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**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<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})
|
||||
})
|
||||
```
|
||||
|
||||
**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)!
|
||||
This script used a boilerplate from [NPWD](https://github.com/project-error/fivem-react-boilerplate-lua). Thank you for the boilerplate!
|
||||
|
@ -3,23 +3,93 @@ local function toggleNuiFrame(shouldShow)
|
||||
SendReactMessage('setVisible', shouldShow)
|
||||
end
|
||||
|
||||
RegisterCommand('show-nui', function()
|
||||
toggleNuiFrame(true)
|
||||
debugPrint('Show NUI frame')
|
||||
function Draw3DText(x, y, z, text)
|
||||
local onScreen, _x, _y = World3dToScreen2d(x, y, z)
|
||||
local px,py,pz=table.unpack(GetGameplayCamCoords())
|
||||
|
||||
if onScreen then
|
||||
SetTextScale(0.35, 0.35)
|
||||
SetTextFont(4)
|
||||
SetTextProportional(1)
|
||||
SetTextColour(255, 255, 255, 215)
|
||||
SetTextDropShadow(0, 0, 0, 55)
|
||||
SetTextEdge(0, 0, 0, 150)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(1)
|
||||
AddTextComponentString(text)
|
||||
DrawText(_x,_y)
|
||||
end
|
||||
end
|
||||
|
||||
function dump(o)
|
||||
if type(o) == 'table' then
|
||||
local s = '{ '
|
||||
for k,v in pairs(o) do
|
||||
if type(k) ~= 'number' then k = '"'..k..'"' end
|
||||
s = s .. '['..k..'] = ' .. dump(v) .. ','
|
||||
end
|
||||
return s .. '} '
|
||||
else
|
||||
return tostring(o)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- When we start up, request the doors from the server
|
||||
local clientDoors = {}
|
||||
RegisterNetEvent('sticks_keypad:setClientDoors')
|
||||
AddEventHandler('sticks_keypad:setClientDoors', function(doors)
|
||||
clientDoors = doors
|
||||
|
||||
-- For each door, create a marker and a text label to allow the user to press "E" to open the keypad
|
||||
for i, v in ipairs(clientDoors) do
|
||||
-- Dump the door data
|
||||
print('[sticks_keypad] Loaded door id: ' .. v[1] .. ' with location vector: ' .. dump(v[2]))
|
||||
|
||||
-- Create a draw thread
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Citizen.Wait(0)
|
||||
local playerPed = PlayerPedId()
|
||||
local playerCoords = GetEntityCoords(playerPed)
|
||||
local doorCoords = vector3(v[2].x, v[2].y, v[2].z)
|
||||
local distance = #(playerCoords - doorCoords)
|
||||
|
||||
if distance < 2.0 then
|
||||
DrawMarker(1, v[2].x, v[2].y, v[2].z + 0.5, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 255, 0, 0, 200, 0, 0, 0, 0)
|
||||
Draw3DText(v[2].x, v[2].y, v[2].z + 0.5, "Press ~g~E~w~ to open the keypad")
|
||||
if IsControlJustPressed(0, 38) then
|
||||
toggleNuiFrame(true)
|
||||
SendReactMessage('sticks_keypad:uiInit', {doorId = v[1], code = v[4]})
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNUICallback('hideFrame', function(_, cb)
|
||||
-- Handle the NUI message from the React app
|
||||
RegisterNUICallback('sticks_keypad:codeSubmitSuccess', function(data, cb)
|
||||
toggleNuiFrame(false)
|
||||
debugPrint('Hide NUI frame')
|
||||
cb({})
|
||||
dump(data)
|
||||
TriggerServerEvent('sticks_keypad:checkCodeAndTeleport', data.doorId, data.code)
|
||||
cb({ok = true})
|
||||
end)
|
||||
|
||||
RegisterNUICallback('getClientData', function(data, cb)
|
||||
debugPrint('Data sent by React', json.encode(data))
|
||||
-- Hide Frame NUICallback
|
||||
RegisterNUICallback('sticks_keypad:hideFrame', function(data, cb)
|
||||
toggleNuiFrame(false)
|
||||
cb({ok = true})
|
||||
end)
|
||||
|
||||
-- Lets send back client coords to the React frame for use
|
||||
local curCoords = GetEntityCoords(PlayerPedId())
|
||||
-- Command to print out the current vector3 position of the player
|
||||
RegisterCommand('getpos', function()
|
||||
local playerPed = PlayerPedId()
|
||||
local playerCoords = GetEntityCoords(playerPed)
|
||||
print(playerCoords.x .. ', ' .. playerCoords.y .. ', ' .. playerCoords.z)
|
||||
end, false)
|
||||
|
||||
local retData <const> = { x = curCoords.x, y = curCoords.y, z = curCoords.z }
|
||||
cb(retData)
|
||||
end)
|
||||
-- Send the event to the server to get the doors
|
||||
TriggerServerEvent('sticks_keypad:getDoors')
|
@ -1,9 +1,9 @@
|
||||
fx_version "cerulean"
|
||||
|
||||
description "Basic React (TypeScript) & Lua Game Scripts Boilerplate"
|
||||
author "Project Error"
|
||||
description "A simple keypad script for FiveM"
|
||||
author "sticksdev"
|
||||
version '1.0.0'
|
||||
repository 'https://github.com/project-error/fivem-react-boilerplate-lua'
|
||||
repository 'https://github.com/SticksDev/sticks-keypad'
|
||||
|
||||
lua54 'yes'
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
-- Add doors using the fomrat
|
||||
-- doors[doorId] = {loc_vector, teleport_vector, code}
|
||||
print("[sticks_keypad] Loading doors...")
|
||||
|
||||
local doors = {
|
||||
-- Example door, feel free to remove this
|
||||
{1, {x = -3029.3825683594, y = 72.813552856445, z = 11.4}, {x = -3031.3232421875, y = 93.021644592285, z = 12.346099853516}, "1234"},
|
||||
}
|
||||
|
||||
-- Print our loaded doors
|
||||
for i, v in ipairs(doors) do
|
||||
print("[sticks_keypad] Loaded door id: " .. v[1] .. " with code: " .. v[4] .. " and teleport vector: " .. v[2].x .. ", " .. v[2].y .. ", " .. v[2].z)
|
||||
end
|
||||
|
||||
print("[sticks_keypad] Loaded " .. #doors .. " doors! Setting up events...")
|
||||
|
||||
-- Event to send the doors configuration to the client
|
||||
RegisterServerEvent("sticks_keypad:getDoors")
|
||||
AddEventHandler("sticks_keypad:getDoors", function()
|
||||
print("[sticks_keypad::audit] Player requested doors")
|
||||
TriggerClientEvent("sticks_keypad:setClientDoors", source, doors)
|
||||
end)
|
||||
|
||||
-- Event to check if the code is correct, returns true if it is, nil if it isn't
|
||||
RegisterServerEvent("sticks_keypad:checkCodeAndTeleport")
|
||||
AddEventHandler("sticks_keypad:checkCodeAndTeleport", function(doorId, code)
|
||||
print("[sticks_keypad::audit] Player is trying to open door id: " .. doorId .. " with code: " .. code)
|
||||
local source = source
|
||||
|
||||
if doors[doorId][4] == code then
|
||||
print("[sticks_keypad::audit] Player opened door id: " .. doorId)
|
||||
-- Teleport the player to the teleport vector
|
||||
SetEntityCoords(source, doors[doorId][3].x, doors[doorId][3].y, doors[doorId][3].z)
|
||||
return true
|
||||
else
|
||||
print("[sticks_keypad::audit] Player failed to open door id: " .. doorId)
|
||||
return nil
|
||||
end
|
||||
end)
|
||||
|
||||
print("[sticks_keypad] Events setup! Ready to go!")
|
4259
web/package-lock.json
generated
Normal file
4259
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,9 +20,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
|
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
web/src/assets/beep.mp3
Normal file
BIN
web/src/assets/beep.mp3
Normal file
Binary file not shown.
BIN
web/src/assets/denied.mp3
Normal file
BIN
web/src/assets/denied.mp3
Normal file
Binary file not shown.
BIN
web/src/assets/unlock.mp3
Normal file
BIN
web/src/assets/unlock.mp3
Normal file
Binary file not shown.
@ -1,26 +0,0 @@
|
||||
.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;
|
||||
}
|
@ -1,66 +1,153 @@
|
||||
import React, { useState } from "react";
|
||||
import "./App.css";
|
||||
import { debugData } from "../utils/debugData";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import React, { useState } from 'react';
|
||||
import { debugData } from '../utils/debugData';
|
||||
import { fetchNui } from '../utils/fetchNui';
|
||||
|
||||
// Import Audios
|
||||
import beep from '../assets/beep.mp3';
|
||||
import deined from '../assets/denied.mp3';
|
||||
import unlock from '../assets/unlock.mp3';
|
||||
import { useNuiEvent } from '../hooks/useNuiEvent';
|
||||
|
||||
// This will set the NUI to visible if we are
|
||||
// developing in browser
|
||||
debugData([
|
||||
{
|
||||
action: "setVisible",
|
||||
data: true,
|
||||
},
|
||||
{
|
||||
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 playKeypadSound = () => {
|
||||
const audio = new Audio(beep);
|
||||
audio.play();
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [clientData, setClientData] = useState<ReturnData | null>(null);
|
||||
const [keypadState, setKeypadState] = useState<string>('');
|
||||
const [code, setCode] = useState<string>('1234');
|
||||
const [keypadId, setKeypadId] = useState<string>('');
|
||||
const [codeValidated, setCodeValidated] = useState<boolean>(false);
|
||||
const [codeState, setCodeState] = useState<boolean>(false);
|
||||
const [submitButtonEnabled, setSubmitButtonEnabled] =
|
||||
useState<boolean>(false);
|
||||
|
||||
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 });
|
||||
});
|
||||
};
|
||||
// Listen for our "uiCodeSetup" event
|
||||
// and set the code state
|
||||
useNuiEvent<{ doorId: string; code: string }>(
|
||||
'sticks_keypad:uiInit',
|
||||
(data) => {
|
||||
console.log('[sticks_keypad:nui::setDoorId] uiInit: init...');
|
||||
setCode(data.code);
|
||||
setKeypadId(data.doorId);
|
||||
|
||||
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} />}
|
||||
console.log(
|
||||
'[sticks_keypad:nui::setDoorId] uiInit: doorId: ',
|
||||
data.doorId,
|
||||
);
|
||||
|
||||
console.log('[sticks_keypad:nui::setDoorId] uiInit done');
|
||||
},
|
||||
);
|
||||
|
||||
async function onCodeSubmit(data: string) {
|
||||
// Load both the denied and unlock audio
|
||||
const deniedAudio = new Audio(deined);
|
||||
const unlockAudio = new Audio(unlock);
|
||||
|
||||
// Check if the data is equal to the code
|
||||
if (data === code) {
|
||||
// Play the unlock audio
|
||||
await unlockAudio.play();
|
||||
// Set the code state to true
|
||||
setCodeState(true);
|
||||
// Send the data to the server
|
||||
fetchNui('sticks_keypad:codeSubmitSuccess', {
|
||||
code: data,
|
||||
doorId: keypadId,
|
||||
});
|
||||
|
||||
// Reset the keypad state
|
||||
setTimeout(() => {
|
||||
setKeypadState('');
|
||||
setCodeState(false);
|
||||
setSubmitButtonEnabled(false);
|
||||
}, 1000);
|
||||
} else {
|
||||
// Play the denied audio
|
||||
deniedAudio.play();
|
||||
setCodeValidated(true);
|
||||
setSubmitButtonEnabled(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setKeypadState('');
|
||||
setCodeValidated(false);
|
||||
setSubmitButtonEnabled(false);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-screen flex justify-center items-center bg-gray-900/40'>
|
||||
<div className='w-80 p-6 bg-zinc-800 rounded-lg shadow-lg'>
|
||||
<div className='text-3xl text-white mb-4 text-center'>
|
||||
Enter Passcode
|
||||
</div>
|
||||
<div className='text-2xl text-white mb-4 text-center'>
|
||||
{codeState
|
||||
? 'Unlocked'
|
||||
: keypadState.length === code.length && codeValidated
|
||||
? 'Code is incorrect'
|
||||
: keypadState}
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2 mb-4'>
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className='w-20 h-20 bg-zinc-700 hover:bg-zinc-700/40 duration-150 text-white text-2xl rounded-lg'
|
||||
onClick={() => {
|
||||
if (keypadState.length < code.length) {
|
||||
setKeypadState((prev) => prev + (i + 1));
|
||||
playKeypadSound();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className='w-20 h-20 bg-zinc-700 hover:bg-zinc-700/40 duration-150 text-white text-2xl rounded-lg'
|
||||
onClick={() => setKeypadState((prev) => prev + '0')}
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
className='w-20 h-20 bg-zinc-700 hover:bg-zinc-700/40 duration-150 text-white text-2xl rounded-lg'
|
||||
onClick={() =>
|
||||
setKeypadState((prev) => prev.slice(0, -1))
|
||||
}
|
||||
>
|
||||
DEL
|
||||
</button>
|
||||
<button
|
||||
className='w-20 h-20 bg-zinc-700 hover:bg-zinc-700/40 duration-150 text-white text-2xl rounded-lg'
|
||||
onClick={() => setKeypadState('')}
|
||||
>
|
||||
C
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className='w-full h-12 bg-green-600 hover:bg-green-700/80 duration-150 text-white text-2xl rounded-lg disabled:bg-gray-500 disabled:cursor-not-allowed'
|
||||
onClick={() => onCodeSubmit(keypadState)}
|
||||
disabled={
|
||||
keypadState.length !== code.length ||
|
||||
submitButtonEnabled
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -1,18 +1,3 @@
|
||||
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;
|
||||
}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
@ -5,9 +5,9 @@ import App from './components/App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<VisibilityProvider>
|
||||
<App />
|
||||
</VisibilityProvider>
|
||||
</React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<VisibilityProvider>
|
||||
<App />
|
||||
</VisibilityProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@ -1,64 +1,67 @@
|
||||
import React, {
|
||||
Context,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useNuiEvent } from "../hooks/useNuiEvent";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { isEnvBrowser } from "../utils/misc";
|
||||
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;
|
||||
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,
|
||||
children,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useNuiEvent<boolean>("setVisible", setVisible);
|
||||
useNuiEvent<boolean>('setVisible', setVisible);
|
||||
|
||||
// Handle pressing escape/backspace
|
||||
useEffect(() => {
|
||||
// Only attach listener when we are visible
|
||||
if (!visible) return;
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (['Backspace', 'Escape'].includes(e.code)) {
|
||||
if (!isEnvBrowser()) fetchNui('sticks_keypad:hideFrame');
|
||||
else setVisible(!visible);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", keyHandler);
|
||||
window.addEventListener('keydown', keyHandler);
|
||||
|
||||
return () => window.removeEventListener("keydown", keyHandler);
|
||||
}, [visible]);
|
||||
return () => window.removeEventListener('keydown', keyHandler);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<VisibilityCtx.Provider
|
||||
value={{
|
||||
visible,
|
||||
setVisible,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ visibility: visible ? "visible" : "hidden", height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</VisibilityCtx.Provider>
|
||||
);
|
||||
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>,
|
||||
);
|
||||
useContext<VisibilityProviderValue>(
|
||||
VisibilityCtx as Context<VisibilityProviderValue>,
|
||||
);
|
||||
|
@ -32,7 +32,6 @@ export async function fetchNui<T = unknown>(
|
||||
: "nui-frame-app";
|
||||
|
||||
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
|
||||
|
||||
const respFormatted = await resp.json();
|
||||
|
||||
return respFormatted;
|
||||
|
8
web/tailwind.config.js
Normal file
8
web/tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user