1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Nordi98 2025-06-25 00:04:15 +02:00
parent be02d05ba8
commit fc7ea910e9
35 changed files with 11992 additions and 1 deletions

View file

@ -0,0 +1 @@
/node_modules

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
{
"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": {
"@emotion/react": "^11.11.4",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mantine/core": "^6.0.21",
"@mantine/dates": "^6.0.21",
"@mantine/form": "^7.10.1",
"@mantine/hooks": "^6.0.21",
"@mantine/modals": "^6.0.21",
"@mantine/styles": "^6.0.21",
"@tabler/icons-react": "^2.47.0",
"html2canvas": "^1.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"sass": "^1.77.4",
"styled-components": "^6.1.11"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"postcss": "^8.4.38",
"postcss-preset-mantine": "^1.15.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5.4.5",
"vite": "^5.2.13"
}
}

4165
resources/[tools]/mt_lib/web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
import React, { useState, useRef } from "react"
import { DEFAULT_THEME, Paper, Text, Divider, Stack, Button } from '@mantine/core'
import { fetchNui } from "../utils/fetchNui"
import { useNuiEvent } from "../hooks/useNuiEvent"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import * as Icons from '@fortawesome/free-solid-svg-icons'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
const Dialogue: React.FC = () => {
const theme = DEFAULT_THEME
const [options, setOptions] = useState([])
const [label, setLabel] = useState('')
const [speech, setSpeech] = useState('')
useNuiEvent<any>('dialogue', (data) => {
setOptions(data.options)
setLabel(data.label)
setSpeech(data.speech)
})
const getIconByName = (iconName: string) => {
const formattedName = `fa${iconName.charAt(0).toUpperCase() + iconName.slice(1).replace(/-./g, (m) => m[1].toUpperCase())}`
return Icons[formattedName as keyof typeof Icons] || Icons.faQuestionCircle
}
return (
<div
style={{
width: '100%',
height: '100%',
margin: -8,
position: 'fixed',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
background: `linear-gradient(180deg, rgba(255, 255, 255, 0) 75%, rgba(0, 0, 0, 0.80) 100%)`
}}
>
<Stack miw={500} maw={1000} m={50} spacing={5}>
<Text size="xl" fw={700} color="white">{label}</Text>
<Paper withBorder p={5} pl={10}>
<Text color="white" size="lg" fw={500}>{speech}</Text>
</Paper>
<Divider />
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 10
}}
>
{options.length > 0 && options.map(({ label, icon, id, close, canInteract }) => (
canInteract && <Button
color="gray"
bg={theme.colors.dark[7]}
leftIcon={<FontAwesomeIcon icon={getIconByName(icon) as IconProp} />}
style={{
border: `1px solid ${theme.colors.dark[4]}`
}}
onClick={() => fetchNui('executeAction', { id, options, close })}
>
{label}
</Button>
))}
</div>
</Stack>
</div>
)
}
export default Dialogue

View file

@ -0,0 +1,62 @@
import React, { useState } from "react"
import { DEFAULT_THEME, Paper, Text, Divider, TypographyStylesProvider } from '@mantine/core'
import { useNuiEvent } from "../hooks/useNuiEvent"
const MissionStatus: React.FC = () => {
const theme = DEFAULT_THEME
const [title, setTitle] = useState('')
const [text, setText] = useState('')
useNuiEvent<any>('missionStatus', (data) => {
setTitle(data.title)
setText(data.text)
})
return (
<div
style={{
width: '100%',
height: '100%',
margin: -8,
position: 'fixed',
display: 'flex',
alignItems: 'center'
}}
>
<div
style={{
maxWidth: 400,
position: 'absolute',
left: 20,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
gap: 5
}}
>
<Text size="xl" color="white" fw={700}>{title}</Text>
<Divider
color="dark.0"
size="lg"
style={{
borderRadius: theme.radius.md
}}
/>
<Paper p={10} withBorder radius="sm">
<TypographyStylesProvider
style={{
textAlign: 'left'
}}
>
<div
dangerouslySetInnerHTML={{ __html: text }}
/>
</TypographyStylesProvider>
</Paper>
</div>
</div>
)
}
export default MissionStatus

View file

@ -0,0 +1,64 @@
import React, { useState } from "react"
import { DEFAULT_THEME, Paper, Text, Kbd } from '@mantine/core'
import { useNuiEvent } from "../hooks/useNuiEvent"
const TextUI: React.FC = () => {
const theme = DEFAULT_THEME
const [key, setKey] = useState('')
const [label, setLabel] = useState('')
const [position, setPosition] = useState('')
useNuiEvent<any>('textUI', (data) => {
setKey(data.key)
setLabel(data.label)
setPosition(data.position)
})
return (
<div
style={{
width: '100%',
height: '100%',
margin: -8,
position: 'fixed',
display: 'flex',
justifyContent: ((position == 'bottom' || position == 'top') ? 'center' : ''),
alignItems: ((position == 'right' || position == 'left') ? 'center' : '')
}}
>
<div
style={{
maxWidth: 400,
position: 'absolute',
bottom: (position == 'bottom' ? 20 : 'auto'),
top: (position == 'top' ? 20 : 'auto'),
right: (position == 'right' ? 20 : 'auto'),
left: (position == 'left' ? 20 : 'auto'),
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
gap: 5
}}
>
<Paper
withBorder
radius="sm"
p={5}
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 5
}}
>
<Kbd>{key}</Kbd>
<Text fw={700}>{label}</Text>
</Paper>
</div>
</div>
)
}
export default TextUI

View file

@ -0,0 +1,74 @@
import React, { useState, useEffect } from "react"
import { DEFAULT_THEME, Paper, Text, Progress } from '@mantine/core'
import { fetchNui } from "../utils/fetchNui"
import { useNuiEvent } from "../hooks/useNuiEvent"
const Timer: React.FC = () => {
const theme = DEFAULT_THEME
const [time, setTime] = useState(0)
const [maxTime, setMaxTime] = useState(60)
const [position, setPosition] = useState('')
const [label, setLabel] = useState('')
useNuiEvent<any>('timer', (data) => {
setLabel(data.label)
setTime(data.time)
setMaxTime(data.time)
setPosition(data.position)
})
useEffect(() => {
if (time > 0) {
const timerInterval = setInterval(() => {
setTime((prevTime) => {
if (prevTime <= 1) {
clearInterval(timerInterval)
fetchNui('finishTimer')
return 0
}
return prevTime - 1
})
}, 1000)
return () => clearInterval(timerInterval)
}
}, [time])
return (
<div
style={{
width: '100%',
height: '100%',
margin: -8,
position: 'fixed',
display: 'flex',
justifyContent: ((position == 'bottom' || position == 'top') ? 'center' : ''),
alignItems: ((position == 'right' || position == 'left') ? 'center' : '')
}}
>
<Paper
withBorder
radius="sm"
p={10}
style={{
maxWidth: 400,
position: 'absolute',
bottom: (position == 'bottom' ? 20 : 'auto'),
top: (position == 'top' ? 20 : 'auto'),
right: (position == 'right' ? 20 : 'auto'),
left: (position == 'left' ? 20 : 'auto'),
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
gap: 5
}}
>
<Text fw={700}>{label}: {time}s</Text>
<Progress w="100%" value={(time / maxTime) * 100} />
</Paper>
</div>
)
}
export default Timer

View file

@ -0,0 +1,35 @@
import { MutableRefObject, useEffect, useRef } from "react";
import { noop } from "../utils/misc";
interface NuiMessageData<T = unknown> {
action: string;
data: T;
}
type NuiHandlerSignature<T> = (data: T) => void;
export const useNuiEvent = <T = unknown>(
action: string,
handler: (data: T) => void,
) => {
const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event: MessageEvent<NuiMessageData<T>>) => {
const { action: eventAction, data } = event.data;
if (savedHandler.current) {
if (eventAction === action) {
savedHandler.current(data);
}
}
};
window.addEventListener("message", eventListener);
return () => window.removeEventListener("message", eventListener);
}, [action]);
};

View file

@ -0,0 +1,31 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { VisibilityProvider } from './providers/VisibilityProvider'
import { MantineProvider } from '@mantine/core'
import { ModalsProvider } from '@mantine/modals'
import { DatesProvider } from '@mantine/dates'
import Dialogue from './components/Dialogue'
import MissionStatus from './components/MissionStatus'
import Timer from './components/Timer'
import TextUI from './components/TextUI'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MantineProvider theme={{ colorScheme:'dark' }}>
<ModalsProvider>
<VisibilityProvider componentName="Dialogue">
<Dialogue />
</VisibilityProvider>
<VisibilityProvider componentName="MissionStatus">
<MissionStatus />
</VisibilityProvider>
<VisibilityProvider componentName="Timer">
<Timer />
</VisibilityProvider>
<VisibilityProvider componentName="TextUI">
<TextUI />
</VisibilityProvider>
</ModalsProvider>
</MantineProvider>
</React.StrictMode>
)

View file

@ -0,0 +1,47 @@
import React, { 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;
}
export const VisibilityProvider: React.FC<{
children: React.ReactNode;
componentName: string;
}> = ({ children, componentName }) => {
const [visible, setVisible] = useState(false);
useNuiEvent<boolean>(`setVisible${componentName}`, setVisible);
useEffect(() => {
const keyHandler = (e: KeyboardEvent) => {
if (visible && componentName !== 'TextUI' && componentName !== 'Timer' && componentName !== 'MissionStatus' && e.code === "Escape") {
if (!isEnvBrowser()) fetchNui("hideFrame", { name: `setVisible${componentName}` });
else setVisible(false);
}
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
}, [visible, componentName]);
return (
<VisibilityCtx.Provider value={{ visible, setVisible }}>
<div style={{ visibility: visible ? "visible" : "hidden" }}>
{children}
</div>
</VisibilityCtx.Provider>
);
};
export const useVisibility = () =>
useContext<VisibilityProviderValue>(
VisibilityCtx as React.Context<VisibilityProviderValue>
);

View file

@ -0,0 +1,30 @@
import { isEnvBrowser } from "./misc";
interface DebugEvent<T = unknown> {
action: string;
data: T;
}
/**
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
* This is used when developing in browser
*
* @param events - The event you want to cover
* @param timer - How long until it should trigger (ms)
*/
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
if (import.meta.env.MODE === "development" && isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
action: event.action,
data: event.data,
},
}),
);
}, timer);
}
}
};

View file

@ -0,0 +1,26 @@
import { isEnvBrowser } from "./misc"
export async function fetchNui<T = unknown>(
eventName: string,
data?: unknown,
mockData?: T,
): Promise<T> {
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
if (isEnvBrowser() && mockData) return mockData
const resourceName = (window as any).GetParentResourceName
? (window as any).GetParentResourceName() : "nui-frame-app"
const resp = await fetch(`https://${resourceName}/${eventName}`, options)
const respFormatted = await resp.json()
return respFormatted
}

View file

@ -0,0 +1,3 @@
export const isEnvBrowser = (): boolean => !(window as any).invokeNative
export const noop = () => {}

View file

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

View file

@ -0,0 +1,22 @@
{
"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" }]
}

View file

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

View file

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