0.0.1
This commit is contained in:
28
resources/[core]/esx_multicharacter/web/eslint.config.js
Normal file
28
resources/[core]/esx_multicharacter/web/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
13
resources/[core]/esx_multicharacter/web/index.html
Normal file
13
resources/[core]/esx_multicharacter/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ESX MULTICHARACTER</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
resources/[core]/esx_multicharacter/web/package.json
Normal file
34
resources/[core]/esx_multicharacter/web/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "vite-react-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
62
resources/[core]/esx_multicharacter/web/src/App.tsx
Normal file
62
resources/[core]/esx_multicharacter/web/src/App.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import CharacterSelection from "./components/CharacterSelection";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNuiEvent } from "./utils/useNuiEvent";
|
||||
import { Character, Locale } from "./types/Character";
|
||||
import { fetchNui } from "./utils/fetchNui";
|
||||
|
||||
function App() {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const [characters, setCharacters] = useState<Character[]>([]);
|
||||
const [Candelete, setCandelete] = useState<boolean>(false);
|
||||
const [MaxAllowedSlot, setMaxAllowedSlot] = useState<number>(0);
|
||||
const [locale, setLocale] = useState<Locale>({
|
||||
char_info_title: "",
|
||||
play: "",
|
||||
title: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchNui("nuiReady");
|
||||
}, []);
|
||||
|
||||
useNuiEvent("ToggleMulticharacter", (data: any) => {
|
||||
if (data.show) {
|
||||
const validCharacters = data.Characters.filter(
|
||||
(char: any) => char !== null
|
||||
);
|
||||
const parsedCharacters: Character[] = validCharacters.map(
|
||||
(char: any, index: number) => ({
|
||||
id: char.id.toString(),
|
||||
name: `${char.firstname} ${char.lastname}`,
|
||||
birthDate: char.dateofbirth,
|
||||
gender: char.sex,
|
||||
occupation: char.job,
|
||||
disabled: char.disabled,
|
||||
isActive: index === 0,
|
||||
})
|
||||
);
|
||||
|
||||
setIsVisible(true);
|
||||
setCharacters(parsedCharacters);
|
||||
setCandelete(data.CanDelete);
|
||||
setMaxAllowedSlot(data.AllowedSlot);
|
||||
setLocale(data.Locale);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
setCharacters([]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
isVisible && (
|
||||
<CharacterSelection
|
||||
initialCharacters={characters}
|
||||
Candelete={Candelete}
|
||||
MaxAllowedSlot={MaxAllowedSlot}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
BIN
resources/[core]/esx_multicharacter/web/src/assets/esxLogo.png
Normal file
BIN
resources/[core]/esx_multicharacter/web/src/assets/esxLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Character } from '../types/Character';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface CharacterCardProps {
|
||||
character: Character;
|
||||
onSelect: (id: string) => void;
|
||||
onInfoClick: (id: string) => void;
|
||||
showInfo: boolean;
|
||||
PlayCharacter: () => void;
|
||||
}
|
||||
|
||||
const CharacterCard: React.FC<CharacterCardProps> = ({
|
||||
character,
|
||||
onSelect,
|
||||
onInfoClick,
|
||||
showInfo,
|
||||
PlayCharacter
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className='flex justify-center items-center mb-4'>
|
||||
<div
|
||||
className={`
|
||||
h-[65px] w-[462px] cursor-pointer rounded-[5px] overflow-hidden group
|
||||
${character.isActive
|
||||
? ''
|
||||
: 'cursor-pointer rounded-[5px] overflow-hidden bg-[#38383880] border-2 border-[#ffffff50] hover:border-[#FFFFFF] hover:bg-[#38383840'}
|
||||
`}
|
||||
onClick={() => onSelect(character.id)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-2 transition-all duration-300
|
||||
${character.isActive ? 'text-[#fb9b04]' : 'text-gray-400 group-hover:text-[#FFFFFF]'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center rounded-[5px] p-4 overflow-hidden
|
||||
${character.isActive
|
||||
? 'h-[65px] bg-[#fb9b0440] border-2 border-[#fb9b04]'
|
||||
: 'h-[60px]'}
|
||||
${character.isActive && showInfo ? 'w-full' : character.isActive ? 'w-[411px]' : 'w-full'}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
icon="material-symbols:person-rounded"
|
||||
width="30"
|
||||
height="30"
|
||||
className={`mr-3 flex-shrink-0`}
|
||||
/>
|
||||
<div className="flex justify-center items-center w-full h-full overflow-hidden">
|
||||
<span className={`font-bold text-[24px] tracking-wide whitespace-nowrap ${character.isActive ? 'truncate' : 'ml-2'}`}>
|
||||
{character.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{character.isActive && (
|
||||
<div className="flex gap-2 flex-shrink-0">
|
||||
<button
|
||||
className={`
|
||||
h-[65px] rounded-[5px] ${character.disabled ? 'bg-gray-500' : 'bg-orange-500 hover:bg-orange-600'} flex text-black items-center justify-center transition-all duration-300
|
||||
${showInfo ? 'w-0 scale-0 opacity-0 overflow-hidden' : 'w-[65px] h-[65px] scale-100 opacity-100'}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
PlayCharacter();
|
||||
}}
|
||||
disabled={character.disabled}
|
||||
>
|
||||
<Icon
|
||||
icon="si:play-fill"
|
||||
width="30"
|
||||
height="30"
|
||||
color="#383838"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="w-[65px] h-[65px] rounded-[5px] bg-orange-500 text-black flex items-center justify-center transition-colors hover:bg-orange-600 flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onInfoClick(character.id);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="bi:info"
|
||||
width="30"
|
||||
height="30"
|
||||
color="#383838"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default CharacterCard;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Info, Cake, User2, Briefcase, Trash2 } from 'lucide-react';
|
||||
import { Character, Locale } from '../types/Character';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface CharacterInfoProps {
|
||||
character: Character;
|
||||
onClose: () => void;
|
||||
isAllowedtoDelete: boolean;
|
||||
PlayCharacter : () => void;
|
||||
handleDelete: () => void;
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const CharacterInfo: React.FC<CharacterInfoProps> = ({ character, onClose, isAllowedtoDelete , PlayCharacter, handleDelete, locale}) => {
|
||||
if (!character) return null;
|
||||
|
||||
return (
|
||||
<div className='flex justify-center items-center'>
|
||||
<div className="w-[462px] bg-neutral-900 rounded-b-lg p-4 animate-slideDown">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-gray-400 uppercase text-sm mb-3 font-semibold flex items-center">
|
||||
<Info className="mr-2" size={16} /> {locale.char_info_title}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-neutral-800 p-3 rounded flex items-center">
|
||||
<Cake className="mr-3 text-white" size={18} />
|
||||
<div className="flex justify-center w-full">
|
||||
<span className="text-white mr-1">{character.birthDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-neutral-800 p-3 rounded flex items-center">
|
||||
<User2 className="mr-3 text-white-400" size={18} />
|
||||
<div className="flex justify-center w-full">
|
||||
<span className="text-white mr-6">{character.gender}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-neutral-800 p-3 rounded flex items-center col-span-2">
|
||||
<Briefcase className="mr-3 text-white-400" size={18} />
|
||||
<div className="flex justify-center w-full">
|
||||
<span className="text-white mr-6">{character.occupation}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`flex-1 ${character.disabled ? 'bg-gray-500' : 'bg-neutral-800 hover:bg-[#FFA31A] hover:text-[#383838]'} text-white py-2 px-4 font-semibold text-[16px] rounded-[5px] flex items-center justify-center transition-colors`}
|
||||
onClick={PlayCharacter}
|
||||
disabled={character.disabled}
|
||||
>
|
||||
<Icon icon="flowbite:play-solid" width={23} height={23} className="mr-2" />
|
||||
{locale.play}
|
||||
</button>
|
||||
{isAllowedtoDelete && (
|
||||
<button
|
||||
className="bg-neutral-800 text-white p-2 rounded hover:bg-red-900 transition-colors"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterInfo;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Character, Locale } from '../types/Character';
|
||||
import CharacterCard from './CharacterCard';
|
||||
import CharacterInfo from './CharacterInfo';
|
||||
import Logo from '../assets/esxLogo.png';
|
||||
import { fetchNui } from '../utils/fetchNui';
|
||||
|
||||
interface CharacterSelectionProps {
|
||||
initialCharacters: Character[];
|
||||
Candelete: boolean;
|
||||
MaxAllowedSlot : number;
|
||||
locale : Locale;
|
||||
}
|
||||
|
||||
const CharacterSelection: React.FC<CharacterSelectionProps> = ({ initialCharacters, Candelete, MaxAllowedSlot, locale }) => {
|
||||
const [characters, setCharacters] = useState<Character[]>(initialCharacters);
|
||||
const [showInfo, setShowInfo] = useState<string | null>(null);
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<Character | null>(
|
||||
characters.find(char => char.isActive) || null
|
||||
);
|
||||
|
||||
const handleSelectCharacter = (id: string) => {
|
||||
if (selectedCharacter?.id === id) return;
|
||||
|
||||
const updatedCharacters = characters.map(char => ({
|
||||
...char,
|
||||
isActive: char.id === id
|
||||
}));
|
||||
|
||||
setCharacters(updatedCharacters);
|
||||
setSelectedCharacter(updatedCharacters.find(char => char.id === id) || null);
|
||||
setShowInfo(null);
|
||||
fetchNui('SelectCharacter', {id : id})
|
||||
};
|
||||
|
||||
const toggleInfo = (id: string) => {
|
||||
setShowInfo(showInfo === id ? null : id);
|
||||
};
|
||||
|
||||
const PlayCharacter = () => {
|
||||
fetchNui('PlayCharacter')
|
||||
}
|
||||
|
||||
const handleCreateCharacter = () => {
|
||||
fetchNui('CreateCharacter')
|
||||
}
|
||||
|
||||
const handleDeleteCharacter = () => {
|
||||
if (!selectedCharacter) return;
|
||||
|
||||
const updatedCharactersRaw = characters.filter(char => char.id !== selectedCharacter.id);
|
||||
|
||||
fetchNui('DeleteCharacter');
|
||||
|
||||
if (updatedCharactersRaw.length > 0) {
|
||||
const updatedCharacters = updatedCharactersRaw.map((char, index) => ({
|
||||
...char,
|
||||
isActive: index === 0
|
||||
}));
|
||||
|
||||
setCharacters(updatedCharacters);
|
||||
setSelectedCharacter(updatedCharacters[0]);
|
||||
setShowInfo(null);
|
||||
} else {
|
||||
setCharacters([]);
|
||||
setSelectedCharacter(null);
|
||||
setShowInfo(null);
|
||||
handleCreateCharacter();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-[#161616F2] text-white w-[527px] border-r border-neutral-800 overflow-hidden animate-slideIn flex flex-col">
|
||||
<div className="p-4 flex-1 overflow-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<img src={Logo} alt="Logo" className="h-16 ml-1" />
|
||||
<h2 className="text-[24px] font-bold tracking-wide text-right flex-1">{locale.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{characters.map(character => (
|
||||
<div key={character.id}>
|
||||
<CharacterCard
|
||||
character={character}
|
||||
onSelect={handleSelectCharacter}
|
||||
onInfoClick={toggleInfo}
|
||||
showInfo={showInfo === character.id}
|
||||
PlayCharacter={PlayCharacter}
|
||||
/>
|
||||
{character.isActive && showInfo === character.id && (
|
||||
<CharacterInfo
|
||||
character={character}
|
||||
onClose={() => setShowInfo(null)}
|
||||
isAllowedtoDelete={Candelete}
|
||||
PlayCharacter={PlayCharacter}
|
||||
handleDelete={handleDeleteCharacter}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-neutral-800">
|
||||
<button
|
||||
className={`w-full h-[70px] ${characters.length >= MaxAllowedSlot ? 'bg-gray-500' : 'bg-[#FB9B04] cursor-pointer hover:bg-orange-600'} text-[#383838] p-3 rounded flex items-center justify-center transition-colors`}
|
||||
onClick={handleCreateCharacter}
|
||||
disabled={characters.length >= MaxAllowedSlot}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterSelection;
|
||||
58
resources/[core]/esx_multicharacter/web/src/index.css
Normal file
58
resources/[core]/esx_multicharacter/web/src/index.css
Normal file
@@ -0,0 +1,58 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--primary: #ff9800;
|
||||
--primary-dark: #f57c00;
|
||||
--dark-bg: #1c1c1c;
|
||||
--card-bg: #262626;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Poppins", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slideIn {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slideDown {
|
||||
animation: slideDown 0.3s ease-out forwards;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
7
resources/[core]/esx_multicharacter/web/src/main.tsx
Normal file
7
resources/[core]/esx_multicharacter/web/src/main.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
birthDate: string;
|
||||
gender: 'MALE' | 'FEMALE';
|
||||
occupation: string;
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface Locale {
|
||||
char_info_title: string;
|
||||
play : string;
|
||||
title : string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isEnvBrowser } from "./misc";
|
||||
|
||||
/**
|
||||
* Simple wrapper around fetch API tailored for CEF/NUI use. This abstraction
|
||||
* can be extended to include AbortController if needed or if the response isn't
|
||||
* JSON. Tailor it to your needs.
|
||||
*
|
||||
* @param eventName - The endpoint eventname to target
|
||||
* @param data - Data you wish to send in the NUI Callback
|
||||
* @param mockData - Mock data to be returned if in the browser
|
||||
*
|
||||
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
|
||||
*/
|
||||
|
||||
export async function fetchNui<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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Will return whether the current environment is in a regular browser
|
||||
// and not CEF
|
||||
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
||||
|
||||
// Basic no operation function
|
||||
export const noop = () => {};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MutableRefObject, useEffect, useRef } from "react";
|
||||
import { noop } from "./misc";
|
||||
|
||||
interface NuiMessageData<T = unknown> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
type NuiHandlerSignature<T> = (data: T) => void;
|
||||
|
||||
/**
|
||||
* A hook that manage events listeners for receiving data from the client scripts
|
||||
* @param action The specific `action` that should be listened for.
|
||||
* @param handler The callback function that will handle data relayed by this hook
|
||||
*
|
||||
* @example
|
||||
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
|
||||
* // whatever logic you want
|
||||
* })
|
||||
*
|
||||
**/
|
||||
|
||||
export const useNuiEvent = <T = unknown>(
|
||||
action: string,
|
||||
handler: (data: T) => void,
|
||||
) => {
|
||||
const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);
|
||||
|
||||
// Make sure we handle for a reactive handler
|
||||
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);
|
||||
// Remove Event Listener on component cleanup
|
||||
return () => window.removeEventListener("message", eventListener);
|
||||
}, [action]);
|
||||
};
|
||||
1
resources/[core]/esx_multicharacter/web/src/vite-env.d.ts
vendored
Normal file
1
resources/[core]/esx_multicharacter/web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
33
resources/[core]/esx_multicharacter/web/tailwind.config.js
Normal file
33
resources/[core]/esx_multicharacter/web/tailwind.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
orange: {
|
||||
500: '#FF9800',
|
||||
600: '#F57C00',
|
||||
},
|
||||
neutral: {
|
||||
800: '#262626',
|
||||
900: '#1c1c1c',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'slide-in': 'slideIn 0.5s ease-out forwards',
|
||||
'slide-down': 'slideDown 0.3s ease-out forwards',
|
||||
},
|
||||
keyframes: {
|
||||
slideIn: {
|
||||
'0%': { transform: 'translateX(-100%)', opacity: 0 },
|
||||
'100%': { transform: 'translateX(0)', opacity: 1 },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { maxHeight: '0', opacity: 0, transform: 'translateY(-10px)' },
|
||||
'100%': { maxHeight: '500px', opacity: 1, transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
24
resources/[core]/esx_multicharacter/web/tsconfig.app.json
Normal file
24
resources/[core]/esx_multicharacter/web/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
resources/[core]/esx_multicharacter/web/tsconfig.json
Normal file
7
resources/[core]/esx_multicharacter/web/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
resources/[core]/esx_multicharacter/web/tsconfig.node.json
Normal file
22
resources/[core]/esx_multicharacter/web/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
resources/[core]/esx_multicharacter/web/vite.config.ts
Normal file
14
resources/[core]/esx_multicharacter/web/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user