mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-18 15:36:02 +01:00
Merge branch 'v3'
This commit is contained in:
@@ -7,7 +7,7 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw
|
|||||||
|
|
||||||
--[[ Resource Information ]]--
|
--[[ Resource Information ]]--
|
||||||
name 'ox_lib'
|
name 'ox_lib'
|
||||||
author 'Linden'
|
author 'Overextended'
|
||||||
version '2.21.0'
|
version '2.21.0'
|
||||||
license 'LGPL-3.0-or-later'
|
license 'LGPL-3.0-or-later'
|
||||||
repository 'https://github.com/overextended/ox_lib'
|
repository 'https://github.com/overextended/ox_lib'
|
||||||
@@ -41,6 +41,7 @@ client_scripts {
|
|||||||
'imports/callback/client.lua',
|
'imports/callback/client.lua',
|
||||||
'imports/requestModel/client.lua',
|
'imports/requestModel/client.lua',
|
||||||
'imports/requestAnimDict/client.lua',
|
'imports/requestAnimDict/client.lua',
|
||||||
|
'imports/addKeybind/client.lua',
|
||||||
'resource/**/client.lua',
|
'resource/**/client.lua',
|
||||||
'resource/**/client/*.lua'
|
'resource/**/client/*.lua'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
if cache.game == 'redm' then return end
|
|
||||||
if not lib.player then lib.player() end
|
|
||||||
|
|
||||||
return function(resource)
|
|
||||||
local ESX = exports[resource]:getSharedObject()
|
|
||||||
|
|
||||||
RegisterNetEvent('esx:playerLoaded', function(xPlayer)
|
|
||||||
ESX.PlayerData = xPlayer
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('esx:setJob', function(job)
|
|
||||||
ESX.PlayerData.job = job
|
|
||||||
end)
|
|
||||||
|
|
||||||
local CPlayer = lib.getPlayer()
|
|
||||||
|
|
||||||
function lib.getPlayer()
|
|
||||||
return setmetatable({
|
|
||||||
id = cache.playerId,
|
|
||||||
serverId = cache.serverId,
|
|
||||||
}, CPlayer)
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:hasGroup(filter)
|
|
||||||
local data = ESX.PlayerData
|
|
||||||
local type = type(filter)
|
|
||||||
|
|
||||||
if type == 'string' then
|
|
||||||
if data.job.name == filter then
|
|
||||||
return data.job.name, data.job.grade
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local tabletype = table.type(filter)
|
|
||||||
|
|
||||||
if tabletype == 'hash' then
|
|
||||||
local grade = filter[data.job.name]
|
|
||||||
|
|
||||||
if grade and grade <= data.job.grade then
|
|
||||||
return data.job.name, data.job.grade
|
|
||||||
end
|
|
||||||
elseif tabletype == 'array' then
|
|
||||||
for i = 1, #filter do
|
|
||||||
if data.job.name == filter[i] then
|
|
||||||
return data.job.name, data.job.grade
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
player = lib.getPlayer()
|
|
||||||
|
|
||||||
return ESX
|
|
||||||
end
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
if cache.game == 'redm' then return end
|
|
||||||
if not lib.player then lib.player() end
|
|
||||||
|
|
||||||
return function(resource)
|
|
||||||
local ESX = exports[resource]:getSharedObject()
|
|
||||||
-- Eventually add some functions here to simplify the creation of framework-agnostic resources.
|
|
||||||
|
|
||||||
local CPlayer = lib.getPlayer()
|
|
||||||
|
|
||||||
function lib.getPlayer(player)
|
|
||||||
player = type(player) == 'table' and player.playerId or ESX.GetPlayerFromId(player)
|
|
||||||
|
|
||||||
if not player then
|
|
||||||
error(("'%s' is not a valid player"):format(player))
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(player, CPlayer)
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:hasGroup(filter)
|
|
||||||
local type = type(filter)
|
|
||||||
|
|
||||||
if type == 'string' then
|
|
||||||
if self.job.name == filter then
|
|
||||||
return self.job.name, self.job.grade
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local tabletype = table.type(filter)
|
|
||||||
|
|
||||||
if tabletype == 'hash' then
|
|
||||||
local grade = filter[self.job.name]
|
|
||||||
|
|
||||||
if grade and grade <= self.job.grade then
|
|
||||||
return self.job.name, self.job.grade
|
|
||||||
end
|
|
||||||
elseif tabletype == 'array' then
|
|
||||||
for i = 1, #filter do
|
|
||||||
if self.job.name == filter[i] then
|
|
||||||
return self.job.name, self.job.grade
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return ESX
|
|
||||||
end
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
if cache.game == 'redm' then return end
|
|
||||||
if not lib.player then lib.player() end
|
|
||||||
|
|
||||||
return function(resource)
|
|
||||||
local QBCore = exports[resource]:GetCoreObject()
|
|
||||||
local PlayerData
|
|
||||||
|
|
||||||
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
|
|
||||||
PlayerData = QBCore.Functions.GetPlayerData()
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(job)
|
|
||||||
PlayerData.job = job
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('QBCore:Client:OnGangUpdate', function(gang)
|
|
||||||
PlayerData.gang = gang
|
|
||||||
end)
|
|
||||||
|
|
||||||
local CPlayer = lib.getPlayer()
|
|
||||||
|
|
||||||
function lib.getPlayer()
|
|
||||||
return setmetatable({
|
|
||||||
id = cache.playerId,
|
|
||||||
serverId = cache.serverId,
|
|
||||||
}, CPlayer)
|
|
||||||
end
|
|
||||||
|
|
||||||
local groups = { 'job', 'gang' }
|
|
||||||
|
|
||||||
function CPlayer:hasGroup(filter)
|
|
||||||
local type = type(filter)
|
|
||||||
|
|
||||||
if type == 'string' then
|
|
||||||
for i = 1, #groups do
|
|
||||||
local data = PlayerData[groups[i]]
|
|
||||||
|
|
||||||
if data.name == filter then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local tabletype = table.type(filter)
|
|
||||||
|
|
||||||
if tabletype == 'hash' then
|
|
||||||
for i = 1, #groups do
|
|
||||||
local data = PlayerData[groups[i]]
|
|
||||||
local grade = filter[data.name]
|
|
||||||
|
|
||||||
if grade and grade <= data.grade.level then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif tabletype == 'array' then
|
|
||||||
for i = 1, #filter do
|
|
||||||
local group = filter[i]
|
|
||||||
|
|
||||||
for j = 1, #groups do
|
|
||||||
local data = PlayerData[groups[j]]
|
|
||||||
|
|
||||||
if data.name == group then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
player = lib.getPlayer()
|
|
||||||
|
|
||||||
return QBCore
|
|
||||||
end
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
if cache.game == 'redm' then return end
|
|
||||||
if not lib.player then lib.player() end
|
|
||||||
|
|
||||||
return function(resource)
|
|
||||||
local QBCore = exports[resource]:GetCoreObject()
|
|
||||||
-- Eventually add some functions here to simplify the creation of framework-agnostic resources.
|
|
||||||
|
|
||||||
local CPlayer = lib.getPlayer()
|
|
||||||
|
|
||||||
function lib.getPlayer(player)
|
|
||||||
player = type(player) == 'table' and player.playerId or QBCore.Functions.GetPlayer(player)
|
|
||||||
|
|
||||||
if not player then
|
|
||||||
error(("'%s' is not a valid player"):format(player))
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(player, CPlayer)
|
|
||||||
end
|
|
||||||
|
|
||||||
local groups = { 'job', 'gang' }
|
|
||||||
|
|
||||||
function CPlayer:hasGroup(filter)
|
|
||||||
local type = type(filter)
|
|
||||||
|
|
||||||
if type == 'string' then
|
|
||||||
for i = 1, #groups do
|
|
||||||
local data = self.PlayerData[groups[i]]
|
|
||||||
|
|
||||||
if data.name == filter then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local tabletype = table.type(filter)
|
|
||||||
|
|
||||||
if tabletype == 'hash' then
|
|
||||||
for i = 1, #groups do
|
|
||||||
local data = self.PlayerData[groups[i]]
|
|
||||||
local grade = filter[data.name]
|
|
||||||
|
|
||||||
if grade and grade <= data.grade.level then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif tabletype == 'array' then
|
|
||||||
for i = 1, #filter do
|
|
||||||
local group = filter[i]
|
|
||||||
|
|
||||||
for j = 1, #groups do
|
|
||||||
local data = self.PlayerData[groups[j]]
|
|
||||||
|
|
||||||
if data.name == group then
|
|
||||||
return data.name, data.grade.level
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return QBCore
|
|
||||||
end
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
if cache.game == 'redm' then return end
|
|
||||||
--[[
|
|
||||||
This module was experimental and won't be worked on or used further.
|
|
||||||
May be removed in the future.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local Core = {
|
|
||||||
Ox = 'ox_core',
|
|
||||||
QB = 'qb-core',
|
|
||||||
ESX = 'es_extended',
|
|
||||||
}
|
|
||||||
|
|
||||||
---@deprecated
|
|
||||||
function lib.getCore()
|
|
||||||
print('^3lib.getCore will be removed in a future update (v3.0.0).^0')
|
|
||||||
local result
|
|
||||||
|
|
||||||
Citizen.CreateThreadNow(function()
|
|
||||||
local framework = GetConvar('framework', '')
|
|
||||||
|
|
||||||
if framework == '' then
|
|
||||||
framework = GetResourceState(Core.Ox):find('start') and Core.Ox
|
|
||||||
or GetResourceState(Core.QB):find('start') and Core.QB
|
|
||||||
or GetResourceState(Core.ESX):find('start') and Core.ESX
|
|
||||||
|
|
||||||
if not framework then
|
|
||||||
error('Unable to determine framework (convar is not set, or resource was renamed)')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local success
|
|
||||||
local import
|
|
||||||
local resource
|
|
||||||
|
|
||||||
if framework == Core.Ox then
|
|
||||||
import = ('imports/%s.lua'):format(lib.context)
|
|
||||||
resource = Core.Ox
|
|
||||||
else
|
|
||||||
import = ('imports/getCore/%s/%s.lua'):format(framework, lib.context)
|
|
||||||
resource = lib.name
|
|
||||||
end
|
|
||||||
|
|
||||||
success, result = pcall(LoadResourceFile, resource, import)
|
|
||||||
|
|
||||||
if not result then
|
|
||||||
error(("Unable to load '@%s/%s'"):format(resource, import))
|
|
||||||
end
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
error(result and result or ("Unable to load '@%s/%s'"):format(resource, import), 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
success, result = load(result, ('@@%s/%s'):format(resource, import))
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
error(result, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
success, result = pcall(success, framework)
|
|
||||||
|
|
||||||
if not success then error(result) end
|
|
||||||
|
|
||||||
if framework == Core.Ox then
|
|
||||||
---@diagnostic disable-next-line: undefined-global
|
|
||||||
result = Ox
|
|
||||||
end
|
|
||||||
|
|
||||||
if not result then
|
|
||||||
error(('no loader exists for %s'):format(framework))
|
|
||||||
elseif type(result) == 'function' then
|
|
||||||
result = result(framework)
|
|
||||||
end
|
|
||||||
|
|
||||||
result.resource = framework
|
|
||||||
end)
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
---@diagnostic disable-next-line: deprecated
|
|
||||||
return lib.getCore
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
--[[
|
|
||||||
This module was experimental and won't be worked on or used further.
|
|
||||||
May be removed in the future.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local CPlayer = {}
|
|
||||||
|
|
||||||
function CPlayer:__index(index, ...)
|
|
||||||
local method = CPlayer[index]
|
|
||||||
|
|
||||||
if method then
|
|
||||||
return function(...)
|
|
||||||
return method(self, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:getCoords(update)
|
|
||||||
if update or not self.coords then
|
|
||||||
self.coords = GetEntityCoords(cache.ped)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self.coords
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:getDistance(coords)
|
|
||||||
return #(self:getCoords() - coords)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@deprecated
|
|
||||||
function lib.getPlayer()
|
|
||||||
return CPlayer
|
|
||||||
end
|
|
||||||
|
|
||||||
return lib.getPlayer
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
--[[
|
|
||||||
This module was experimental and won't be worked on or used further.
|
|
||||||
May be removed in the future.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local CPlayer = {}
|
|
||||||
|
|
||||||
function CPlayer:__index(index, ...)
|
|
||||||
local method = CPlayer[index]
|
|
||||||
|
|
||||||
if method then
|
|
||||||
return function(...)
|
|
||||||
return method(self, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:getCoords(update)
|
|
||||||
if update or not self.coords then
|
|
||||||
self.coords = GetEntityCoords(self.getPed())
|
|
||||||
end
|
|
||||||
|
|
||||||
return self.coords
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:getDistance(coords)
|
|
||||||
return #(self:getCoords() - coords)
|
|
||||||
end
|
|
||||||
|
|
||||||
function CPlayer:getPed()
|
|
||||||
self.ped = GetPlayerPed(self.source)
|
|
||||||
return self.ped
|
|
||||||
end
|
|
||||||
|
|
||||||
---@deprecated
|
|
||||||
function lib.getPlayer()
|
|
||||||
return CPlayer
|
|
||||||
end
|
|
||||||
|
|
||||||
return lib.getPlayer
|
|
||||||
2
init.lua
2
init.lua
@@ -76,8 +76,6 @@ end
|
|||||||
|
|
||||||
lib = setmetatable({
|
lib = setmetatable({
|
||||||
name = ox_lib,
|
name = ox_lib,
|
||||||
---@deprecated
|
|
||||||
service = context,
|
|
||||||
context = context,
|
context = context,
|
||||||
exports = {},
|
exports = {},
|
||||||
onCache = function(key, cb)
|
onCache = function(key, cb)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from './interface/notify';
|
|||||||
export * from './interface/progress';
|
export * from './interface/progress';
|
||||||
export * from './interface/textui';
|
export * from './interface/textui';
|
||||||
export * from './interface/skillcheck';
|
export * from './interface/skillcheck';
|
||||||
|
export * from './interface/radial';
|
||||||
|
|
||||||
export * from './streaming';
|
export * from './streaming';
|
||||||
export * from './vehicleProperties';
|
export * from './vehicleProperties';
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ interface AlertDialogProps {
|
|||||||
header: string;
|
header: string;
|
||||||
content: string;
|
content: string;
|
||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
overflow?: boolean;
|
||||||
cancel?: boolean;
|
cancel?: boolean;
|
||||||
labels?: {
|
labels?: {
|
||||||
cancel?: string;
|
cancel?: string;
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
||||||
|
|
||||||
interface ContextMenuItem {
|
interface ContextMenuItem {
|
||||||
title?: string;
|
|
||||||
menu?: string;
|
menu?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
arrow?: boolean;
|
||||||
|
image?: string;
|
||||||
icon?: IconName | [IconPrefix, IconName];
|
icon?: IconName | [IconPrefix, IconName];
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
|
progress?: number;
|
||||||
|
colorScheme?: string;
|
||||||
onSelect?: (args: any) => void;
|
onSelect?: (args: any) => void;
|
||||||
arrow?: boolean;
|
metadata?: string[] | { [key: string]: any } | { label: string; value: any; progress?: number }[];
|
||||||
description?: string;
|
|
||||||
metadata?: string | { [key: string]: any } | string[];
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
event?: string;
|
event?: string;
|
||||||
serverEvent?: string;
|
serverEvent?: string;
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
||||||
|
|
||||||
|
// Should really be improved at some point to only display properties depending on the input type
|
||||||
interface InputDialogRowProps {
|
interface InputDialogRowProps {
|
||||||
type: 'input' | 'number' | 'checkbox' | 'select' | 'slider';
|
type:
|
||||||
|
| 'input'
|
||||||
|
| 'number'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'select'
|
||||||
|
| 'multi-select'
|
||||||
|
| 'slider'
|
||||||
|
| 'color'
|
||||||
|
| 'date'
|
||||||
|
| 'date-range'
|
||||||
|
| 'time'
|
||||||
|
| 'text-area';
|
||||||
label: string;
|
label: string;
|
||||||
options?: { value: string; label: string; default?: string }[];
|
options?: { value: string; label: string; default?: string }[];
|
||||||
password?: boolean;
|
password?: boolean;
|
||||||
@@ -13,13 +25,19 @@ interface InputDialogRowProps {
|
|||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
autosize?: boolean;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
required?: boolean;
|
||||||
|
format?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type inputDialog = (
|
type inputDialog = (
|
||||||
heading: string,
|
heading: string,
|
||||||
rows: string[] | InputDialogRowProps[]
|
rows: string[] | InputDialogRowProps[],
|
||||||
|
options: {
|
||||||
|
allowCancel?: boolean;
|
||||||
|
}
|
||||||
) => Promise<Array<string | number | boolean> | undefined>;
|
) => Promise<Array<string | number | boolean> | undefined>;
|
||||||
export const inputDialog: inputDialog = async (heading, rows) => await exports.ox_lib.inputDialog(heading, rows);
|
export const inputDialog: inputDialog = async (heading, rows) => await exports.ox_lib.inputDialog(heading, rows);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
||||||
|
|
||||||
type NotificationPosition = 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left';
|
type NotificationPosition =
|
||||||
|
| 'top'
|
||||||
|
| 'top-right'
|
||||||
|
| 'top-left'
|
||||||
|
| 'bottom'
|
||||||
|
| 'bottom-right'
|
||||||
|
| 'bottom-left'
|
||||||
|
| 'center-right'
|
||||||
|
| 'center-left';
|
||||||
type NotificationType = 'inform' | 'error' | 'success';
|
type NotificationType = 'inform' | 'error' | 'success';
|
||||||
|
|
||||||
interface NotifyProps {
|
interface NotifyProps {
|
||||||
id?: string;
|
id?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
@@ -18,6 +26,7 @@ interface NotifyProps {
|
|||||||
|
|
||||||
export const notify = (data: NotifyProps): void => exports.ox_lib.notify(data);
|
export const notify = (data: NotifyProps): void => exports.ox_lib.notify(data);
|
||||||
|
|
||||||
|
// Keep for backwards compat with v2
|
||||||
interface DefaultNotifyProps {
|
interface DefaultNotifyProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
18
package/client/resource/interface/radial.ts
Normal file
18
package/client/resource/interface/radial.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types';
|
||||||
|
|
||||||
|
type RadialItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: IconName | [IconPrefix, IconName];
|
||||||
|
onSelect?: () => void;
|
||||||
|
menu?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addRadialItem = (items: RadialItem | RadialItem[]) => exports.ox_lib.addRadialItem(items);
|
||||||
|
|
||||||
|
export const removeRadialItem = (item: string) => exports.ox_lib.removeRadialItem(item);
|
||||||
|
|
||||||
|
export const registerRadial = (radial: { id: string; items: Omit<RadialItem, 'id'>[] }) =>
|
||||||
|
exports.ox_lib.registerRadial(radial);
|
||||||
|
|
||||||
|
export const hideRadial = () => exports.ox_lib.hideRadial();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
type SkillCheckDifficulty = 'easy' | 'medium' | 'hard' | { areaSize: number; speedMultiplier: number };
|
type SkillCheckDifficulty = 'easy' | 'medium' | 'hard' | { areaSize: number; speedMultiplier: number };
|
||||||
|
|
||||||
export const skillCheck = (difficulty: SkillCheckDifficulty | SkillCheckDifficulty[]) =>
|
export const skillCheck = (difficulty: SkillCheckDifficulty | SkillCheckDifficulty[], inputs?: string[]) =>
|
||||||
exports.ox_lib.skillCheck(difficulty);
|
exports.ox_lib.skillCheck(difficulty);
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
"@citizenfx/server": "2.0.5885-1",
|
"@citizenfx/server": "2.0.5885-1",
|
||||||
"@fortawesome/fontawesome-common-types": "6.1.1",
|
"@fortawesome/fontawesome-common-types": "6.1.1",
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.26",
|
||||||
"typescript": "^4.8.3"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.7.1"
|
"prettier": "^2.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
package/pnpm-lock.yaml
generated
24
package/pnpm-lock.yaml
generated
@@ -5,20 +5,20 @@ specifiers:
|
|||||||
'@citizenfx/server': 2.0.5885-1
|
'@citizenfx/server': 2.0.5885-1
|
||||||
'@fortawesome/fontawesome-common-types': 6.1.1
|
'@fortawesome/fontawesome-common-types': 6.1.1
|
||||||
'@types/node': 16.9.1
|
'@types/node': 16.9.1
|
||||||
'@types/react': ^18.0.20
|
'@types/react': ^18.0.26
|
||||||
prettier: ^2.7.1
|
prettier: ^2.8.1
|
||||||
typescript: ^4.8.3
|
typescript: ^4.9.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@citizenfx/client': 2.0.5885-1
|
'@citizenfx/client': 2.0.5885-1
|
||||||
'@citizenfx/server': 2.0.5885-1
|
'@citizenfx/server': 2.0.5885-1
|
||||||
'@fortawesome/fontawesome-common-types': 6.1.1
|
'@fortawesome/fontawesome-common-types': 6.1.1
|
||||||
'@types/node': 16.9.1
|
'@types/node': 16.9.1
|
||||||
'@types/react': 18.0.20
|
'@types/react': 18.0.26
|
||||||
typescript: 4.8.3
|
typescript: 4.9.4
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
prettier: 2.7.1
|
prettier: 2.8.1
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/react/18.0.20:
|
/@types/react/18.0.26:
|
||||||
resolution: {integrity: sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==}
|
resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prop-types': 15.7.5
|
'@types/prop-types': 15.7.5
|
||||||
'@types/scheduler': 0.16.2
|
'@types/scheduler': 0.16.2
|
||||||
@@ -60,14 +60,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
|
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prettier/2.7.1:
|
/prettier/2.8.1:
|
||||||
resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
|
resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/typescript/4.8.3:
|
/typescript/4.9.4:
|
||||||
resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
|
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
|
||||||
engines: {node: '>=4.2.0'}
|
engines: {node: '>=4.2.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
@@ -10,3 +10,10 @@ function RegisterCommand(commandName, callback, restricted)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RegisterNUICallback('getConfig', function(_, cb)
|
||||||
|
cb({
|
||||||
|
primaryColor = GetConvar('ox:primaryColor', 'blue'),
|
||||||
|
primaryShade = GetConvarInt('ox:primaryShade', 8)
|
||||||
|
})
|
||||||
|
end)
|
||||||
@@ -10,6 +10,8 @@ end
|
|||||||
---@field header string;
|
---@field header string;
|
||||||
---@field content string;
|
---@field content string;
|
||||||
---@field centered? boolean?;
|
---@field centered? boolean?;
|
||||||
|
---@field size? 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
---@field overflow? boolean?;
|
||||||
---@field cancel? boolean?;
|
---@field cancel? boolean?;
|
||||||
---@field labels? {cancel?: string, confirm?: string}
|
---@field labels? {cancel?: string, confirm?: string}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local input
|
local input
|
||||||
|
|
||||||
---@class InputDialogRowProps
|
---@class InputDialogRowProps
|
||||||
---@field type 'input' | 'number' | 'checkbox' | 'select' | 'slider'
|
---@field type 'input' | 'number' | 'checkbox' | 'select' | 'slider' | 'multi-select' | 'date' | 'date-range' | 'time' | 'textarea'
|
||||||
---@field label string
|
---@field label string
|
||||||
---@field options? { value: string, label: string, default?: string }[]
|
---@field options? { value: string, label: string, default?: string }[]
|
||||||
---@field password? boolean
|
---@field password? boolean
|
||||||
@@ -14,12 +14,20 @@ local input
|
|||||||
---@field min? number
|
---@field min? number
|
||||||
---@field max? number
|
---@field max? number
|
||||||
---@field step? number
|
---@field step? number
|
||||||
|
---@field autosize? boolean
|
||||||
|
---@field required? boolean
|
||||||
|
---@field format? string
|
||||||
|
---@field clearable? string
|
||||||
---@field description? string
|
---@field description? string
|
||||||
|
|
||||||
|
---@class InputDialogOptionsProps
|
||||||
|
---@field allowCancel? boolean
|
||||||
|
|
||||||
---@param heading string
|
---@param heading string
|
||||||
---@param rows string[] | InputDialogRowProps[]
|
---@param rows string[] | InputDialogRowProps[]
|
||||||
|
---@param options InputDialogOptionsProps[]
|
||||||
---@return string[] | number[] | boolean[] | nil
|
---@return string[] | number[] | boolean[] | nil
|
||||||
function lib.inputDialog(heading, rows)
|
function lib.inputDialog(heading, rows, options)
|
||||||
if input then return end
|
if input then return end
|
||||||
input = promise.new()
|
input = promise.new()
|
||||||
|
|
||||||
@@ -35,7 +43,8 @@ function lib.inputDialog(heading, rows)
|
|||||||
action = 'openDialog',
|
action = 'openDialog',
|
||||||
data = {
|
data = {
|
||||||
heading = heading,
|
heading = heading,
|
||||||
rows = rows
|
rows = rows,
|
||||||
|
options = options
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---@alias NotificationPosition 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left'
|
---@alias NotificationPosition 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left' | 'center-right' | 'center-left'
|
||||||
---@alias NotificationType 'inform' | 'error' | 'success'
|
---@alias NotificationType 'inform' | 'error' | 'success'
|
||||||
|
|
||||||
---@class NotifyProps
|
---@class NotifyProps
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
---@param data NotifyProps
|
---@param data NotifyProps
|
||||||
function lib.notify(data)
|
function lib.notify(data)
|
||||||
SendNUIMessage({
|
SendNUIMessage({
|
||||||
action = 'customNotify',
|
action = 'notify',
|
||||||
data = data
|
data = data
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -30,10 +30,10 @@ end
|
|||||||
|
|
||||||
---@param data DefaultNotifyProps
|
---@param data DefaultNotifyProps
|
||||||
function lib.defaultNotify(data)
|
function lib.defaultNotify(data)
|
||||||
SendNUIMessage({
|
-- Backwards compat for v3
|
||||||
action = 'notify',
|
data.type = data.status
|
||||||
data = data
|
if data.type == 'info' or data.type == 'warning' then data.type = 'inform' end
|
||||||
})
|
return lib.notify(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
RegisterNetEvent('ox_lib:notify', lib.notify)
|
RegisterNetEvent('ox_lib:notify', lib.notify)
|
||||||
|
|||||||
@@ -198,8 +198,6 @@ end
|
|||||||
function lib.cancelProgress()
|
function lib.cancelProgress()
|
||||||
if not progress then
|
if not progress then
|
||||||
error('No progress bar is active')
|
error('No progress bar is active')
|
||||||
elseif not progress.canCancel then
|
|
||||||
error('Progress bar cannot be cancelled')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
progress = false
|
progress = false
|
||||||
|
|||||||
173
resource/interface/client/radial.lua
Normal file
173
resource/interface/client/radial.lua
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
local isOpen = false
|
||||||
|
local menus = {}
|
||||||
|
local menuItems = {}
|
||||||
|
local currentRadial = nil
|
||||||
|
|
||||||
|
---@class RadialMenuItem
|
||||||
|
---@field id string
|
||||||
|
---@field icon string
|
||||||
|
---@field label string
|
||||||
|
---@field menu? string
|
||||||
|
---@field onSelect? function
|
||||||
|
|
||||||
|
---@class RadialMenuProps
|
||||||
|
---@field id string
|
||||||
|
---@field items RadialMenuItem[]
|
||||||
|
|
||||||
|
---Registers a radial sub menu with predefined options.
|
||||||
|
---@param radial RadialMenuProps
|
||||||
|
function lib.registerRadial(radial)
|
||||||
|
menus[radial.id] = radial
|
||||||
|
end
|
||||||
|
|
||||||
|
---Open a registered radial submenu with the given id.
|
||||||
|
---@param id string
|
||||||
|
local function showRadial(id)
|
||||||
|
local radial = menus[id]
|
||||||
|
|
||||||
|
if not radial then return error('No radial menu with such id found.') end
|
||||||
|
|
||||||
|
currentRadial = radial
|
||||||
|
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'openRadialMenu',
|
||||||
|
data = {
|
||||||
|
items = radial.items,
|
||||||
|
sub = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function lib.hideRadial()
|
||||||
|
if not isOpen then return end
|
||||||
|
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'openRadialMenu',
|
||||||
|
data = false
|
||||||
|
})
|
||||||
|
|
||||||
|
SetNuiFocus(false, false)
|
||||||
|
|
||||||
|
isOpen = false
|
||||||
|
currentRadial = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Registers an item or array of items in the global radial menu.
|
||||||
|
---@param items RadialMenuItem | RadialMenuItem[]
|
||||||
|
function lib.addRadialItem(items)
|
||||||
|
local menuSize = #menuItems
|
||||||
|
local invokingResource = GetInvokingResource()
|
||||||
|
|
||||||
|
if table.type(items) == 'array' then
|
||||||
|
for i = 1, #items do
|
||||||
|
local item = items[i]
|
||||||
|
item.resource = invokingResource
|
||||||
|
menuSize += 1
|
||||||
|
menuItems[menuSize] = item
|
||||||
|
end
|
||||||
|
else
|
||||||
|
items.resource = invokingResource
|
||||||
|
menuItems[menuSize + 1] = items
|
||||||
|
end
|
||||||
|
|
||||||
|
if isOpen then
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'refreshItems',
|
||||||
|
data = menuItems
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Removes an item from the global radial menu with the given id.
|
||||||
|
---@param id string
|
||||||
|
function lib.removeRadialItem(id)
|
||||||
|
for i = 1, #menuItems do
|
||||||
|
local item = menuItems[i]
|
||||||
|
if item.id == id then
|
||||||
|
table.remove(menuItems, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if isOpen then
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'refreshItems',
|
||||||
|
data = menuItems
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RegisterNUICallback('radialClick', function(index, cb)
|
||||||
|
cb(1)
|
||||||
|
local item = not currentRadial and menuItems[index + 1] or currentRadial.items[index + 1]
|
||||||
|
|
||||||
|
if item.onSelect then item.onSelect() end
|
||||||
|
if item.menu then return showRadial(item.menu) end
|
||||||
|
|
||||||
|
lib.hideRadial()
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNUICallback('radialBack', function(_, cb)
|
||||||
|
cb(1)
|
||||||
|
if currentRadial.menu then
|
||||||
|
return showRadial(currentRadial.menu)
|
||||||
|
end
|
||||||
|
|
||||||
|
currentRadial = nil
|
||||||
|
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'openRadialMenu',
|
||||||
|
data = {
|
||||||
|
items = menuItems
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNUICallback('radialClose', function(_, cb)
|
||||||
|
cb(1)
|
||||||
|
|
||||||
|
if not isOpen then return end
|
||||||
|
|
||||||
|
SetNuiFocus(false, false)
|
||||||
|
|
||||||
|
isOpen = false
|
||||||
|
currentRadial = nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
lib.addKeybind({
|
||||||
|
name = 'ox_lib-radial',
|
||||||
|
description = 'Open radial menu',
|
||||||
|
defaultKey = 'z',
|
||||||
|
onPressed = function()
|
||||||
|
if isOpen or #menuItems == 0 or IsNuiFocused() or IsPauseMenuActive() then return end
|
||||||
|
|
||||||
|
isOpen = true
|
||||||
|
|
||||||
|
SendNUIMessage({
|
||||||
|
action = 'openRadialMenu',
|
||||||
|
data = {
|
||||||
|
items = menuItems
|
||||||
|
}
|
||||||
|
})
|
||||||
|
SetNuiFocus(true, true)
|
||||||
|
SetNuiFocusKeepInput(true)
|
||||||
|
SetCursorLocation(0.5, 0.5)
|
||||||
|
|
||||||
|
while isOpen do
|
||||||
|
DisablePlayerFiring(cache.playerId, true)
|
||||||
|
DisableControlAction(0, 1, true)
|
||||||
|
DisableControlAction(0, 2, true)
|
||||||
|
Wait(0)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onReleased = lib.hideRadial,
|
||||||
|
})
|
||||||
|
|
||||||
|
AddEventHandler('onClientResourceStop', function(resource)
|
||||||
|
for i = #menuItems, 1, -1 do
|
||||||
|
local item = menuItems[i]
|
||||||
|
|
||||||
|
if item.resource == resource then
|
||||||
|
table.remove(menuItems, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -4,15 +4,19 @@ local skillcheck
|
|||||||
---@alias SkillCheckDifficulity 'easy' | 'medium' | 'hard' | { areaSize: number, speedMultiplier: number }
|
---@alias SkillCheckDifficulity 'easy' | 'medium' | 'hard' | { areaSize: number, speedMultiplier: number }
|
||||||
|
|
||||||
---@param difficulty SkillCheckDifficulity | SkillCheckDifficulity[]
|
---@param difficulty SkillCheckDifficulity | SkillCheckDifficulity[]
|
||||||
|
---@param inputs string[]
|
||||||
---@return boolean?
|
---@return boolean?
|
||||||
function lib.skillCheck(difficulty)
|
function lib.skillCheck(difficulty, inputs)
|
||||||
if skillcheck then return end
|
if skillcheck then return end
|
||||||
skillcheck = promise:new()
|
skillcheck = promise:new()
|
||||||
|
|
||||||
SetNuiFocus(true, false)
|
SetNuiFocus(true, false)
|
||||||
SendNUIMessage({
|
SendNUIMessage({
|
||||||
action = 'startSkillCheck',
|
action = 'startSkillCheck',
|
||||||
data = difficulty
|
data = {
|
||||||
|
difficulty = difficulty,
|
||||||
|
inputs = inputs
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Citizen.Await(skillcheck)
|
return Citizen.Await(skillcheck)
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "ox_lib",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"homepage": "web/build",
|
"homepage": "web/build",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "2.3.6",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/react": "^11.8.2",
|
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/react-fontawesome": "^0.1.19",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@mantine/core": "^5.10.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
"@mantine/dates": "^5.10.0",
|
||||||
"@testing-library/jest-dom": "^5.16.3",
|
"@mantine/hooks": "^5.10.0",
|
||||||
"@testing-library/react": "^12.1.4",
|
"@vitejs/plugin-react": "^3.0.1",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"dayjs": "^1.11.7",
|
||||||
"@types/jest": "^26.0.24",
|
|
||||||
"@types/node": "^16.11.26",
|
|
||||||
"@types/react": "^18.0.24",
|
|
||||||
"@types/react-dom": "^18.0.8",
|
|
||||||
"@vitejs/plugin-react": "^1.3.2",
|
|
||||||
"focus-trap-react": "^9.0.2",
|
"focus-trap-react": "^9.0.2",
|
||||||
"framer-motion": "^6.2.8",
|
"framer-motion": "^8.0.2",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.41.3",
|
||||||
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-markdown": "^8.0.1",
|
"react-markdown": "^8.0.1",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"vite": "^2.9.13",
|
"vite": "^4.0.4",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -36,33 +34,23 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": ">=7.0.0 <8.0.0",
|
"@babel/core": ">=7.0.0 <8.0.0",
|
||||||
"@babel/plugin-syntax-flow": "^7.14.5",
|
"@babel/plugin-syntax-flow": "^7.14.5",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.14.9",
|
"@babel/plugin-transform-react-jsx": "^7.14.9",
|
||||||
"@testing-library/dom": "^8.11.4",
|
|
||||||
"autoprefixer": "^10.0.2",
|
"autoprefixer": "^10.0.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"csstype": "^3.0.10",
|
||||||
"postcss": "^8.1.0",
|
"postcss": "^8.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"rimraf": "^3.0.2"
|
"rimraf": "^3.0.2",
|
||||||
|
"@types/node": "^16.11.26",
|
||||||
|
"@types/react": "^18.0.24",
|
||||||
|
"@types/react-dom": "^18.0.8"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"react-hot-toast@2.4.0": "patches/react-hot-toast@2.4.0.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
228
web/patches/react-hot-toast@2.4.0.patch
Normal file
228
web/patches/react-hot-toast@2.4.0.patch
Normal file
File diff suppressed because one or more lines are too long
3682
web/pnpm-lock.yaml
generated
3682
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,14 @@ import ListMenu from './features/menu/list';
|
|||||||
import Dev from './features/dev';
|
import Dev from './features/dev';
|
||||||
import { isEnvBrowser } from './utils/misc';
|
import { isEnvBrowser } from './utils/misc';
|
||||||
import SkillCheck from './features/skillcheck';
|
import SkillCheck from './features/skillcheck';
|
||||||
|
import RadialMenu from './features/menu/radial';
|
||||||
|
import { theme } from './theme';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { useConfig } from './providers/ConfigProvider';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
const { config } = useConfig();
|
||||||
|
|
||||||
useNuiEvent('setClipboard', (data: string) => {
|
useNuiEvent('setClipboard', (data: string) => {
|
||||||
setClipboard(data);
|
setClipboard(data);
|
||||||
});
|
});
|
||||||
@@ -21,7 +27,7 @@ const App: React.FC = () => {
|
|||||||
fetchNui('init');
|
fetchNui('init');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MantineProvider withNormalizeCSS withGlobalStyles theme={{ ...theme, ...config }}>
|
||||||
<Progressbar />
|
<Progressbar />
|
||||||
<CircleProgressbar />
|
<CircleProgressbar />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
@@ -30,9 +36,10 @@ const App: React.FC = () => {
|
|||||||
<AlertDialog />
|
<AlertDialog />
|
||||||
<ContextMenu />
|
<ContextMenu />
|
||||||
<ListMenu />
|
<ListMenu />
|
||||||
|
<RadialMenu />
|
||||||
<SkillCheck />
|
<SkillCheck />
|
||||||
{isEnvBrowser() && <Dev />}
|
{isEnvBrowser() && <Dev />}
|
||||||
</>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
import { AlertProps } from '../../dialog/AlertDialog';
|
import { AlertProps } from '../../../typings';
|
||||||
|
|
||||||
export const debugAlert = () => {
|
export const debugAlert = () => {
|
||||||
debugData<AlertProps>([
|
debugData<AlertProps>([
|
||||||
@@ -9,11 +9,13 @@ export const debugAlert = () => {
|
|||||||
header: 'Hello there',
|
header: 'Hello there',
|
||||||
content: 'General kenobi \n Markdown works',
|
content: 'General kenobi \n Markdown works',
|
||||||
centered: true,
|
centered: true,
|
||||||
|
size: 'lg',
|
||||||
|
overflow: true,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
labels: {
|
// labels: {
|
||||||
confirm: 'Ok',
|
// confirm: 'Ok',
|
||||||
cancel: 'Not ok',
|
// cancel: 'Not ok',
|
||||||
},
|
// },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ContextMenuProps } from '../../../interfaces/context';
|
import { ContextMenuProps } from '../../../typings';
|
||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
|
|
||||||
export const debugContext = () => {
|
export const debugContext = () => {
|
||||||
@@ -9,6 +9,32 @@ export const debugContext = () => {
|
|||||||
title: 'Vehicle garage',
|
title: 'Vehicle garage',
|
||||||
options: [
|
options: [
|
||||||
{ title: 'Empty button' },
|
{ title: 'Empty button' },
|
||||||
|
{
|
||||||
|
title: 'Karin Kuruma',
|
||||||
|
image: 'https://cdn.discordapp.com/attachments/1063098499027173461/1064276343585505330/screenshot.jpg',
|
||||||
|
arrow: true,
|
||||||
|
colorScheme: 'blue',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
['label']: 'Body',
|
||||||
|
['value']: '55%',
|
||||||
|
['progress']: 55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
['label']: 'Engine',
|
||||||
|
['value']: '100%',
|
||||||
|
['progress']: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
['label']: 'Oil',
|
||||||
|
['progress']: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
['label']: 'Fuel',
|
||||||
|
['progress']: 87,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Example button',
|
title: 'Example button',
|
||||||
description: 'Example button description',
|
description: 'Example button description',
|
||||||
@@ -30,7 +56,7 @@ export const debugContext = () => {
|
|||||||
progress: 80,
|
progress: 80,
|
||||||
icon: 'car-side',
|
icon: 'car-side',
|
||||||
metadata: [{ label: 'Durability', value: '80%' }],
|
metadata: [{ label: 'Durability', value: '80%' }],
|
||||||
colorScheme: 'blue'
|
colorScheme: 'blue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Menu button',
|
title: 'Menu button',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
import { InputProps } from '../../dialog/InputDialog';
|
import type { InputProps } from '../../../typings';
|
||||||
|
|
||||||
export const debugInput = () => {
|
export const debugInput = () => {
|
||||||
debugData<InputProps>([
|
debugData<InputProps>([
|
||||||
@@ -14,6 +14,12 @@ export const debugInput = () => {
|
|||||||
placeholder: '420',
|
placeholder: '420',
|
||||||
description: 'Description that tells you what this input field does',
|
description: 'Description that tells you what this input field does',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
format: '12',
|
||||||
|
label: 'Locker Time',
|
||||||
|
description: 'Description that tells you what this input field does',
|
||||||
|
},
|
||||||
{ type: 'checkbox', label: 'Some checkbox' },
|
{ type: 'checkbox', label: 'Some checkbox' },
|
||||||
{ type: 'input', label: 'Locker PIN', password: true, icon: 'lock' },
|
{ type: 'input', label: 'Locker PIN', password: true, icon: 'lock' },
|
||||||
{ type: 'checkbox', label: 'Some other checkbox', checked: true },
|
{ type: 'checkbox', label: 'Some other checkbox', checked: true },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
import { MenuSettings } from '../../menu/list';
|
import { MenuSettings } from '../../../typings';
|
||||||
|
|
||||||
export const debugMenu = () => {
|
export const debugMenu = () => {
|
||||||
debugData<MenuSettings>([
|
debugData<MenuSettings>([
|
||||||
@@ -34,7 +34,7 @@ export const debugMenu = () => {
|
|||||||
icon: 'car-side',
|
icon: 'car-side',
|
||||||
description: 'Durability: 80%',
|
description: 'Durability: 80%',
|
||||||
colorScheme: 'blue',
|
colorScheme: 'blue',
|
||||||
iconColor: '#55778d'
|
iconColor: '#55778d',
|
||||||
},
|
},
|
||||||
{ label: 'Option 1' },
|
{ label: 'Option 1' },
|
||||||
{ label: 'Option 2' },
|
{ label: 'Option 2' },
|
||||||
|
|||||||
@@ -1,31 +1,48 @@
|
|||||||
import { CustomNotificationProps, NotificationProps } from '../../notifications/NotificationWrapper';
|
import { NotificationProps } from '../../../typings';
|
||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
|
|
||||||
export const debugNotification = () => {
|
export const debugCustomNotification = () => {
|
||||||
debugData<NotificationProps>([
|
debugData<NotificationProps>([
|
||||||
{
|
{
|
||||||
action: 'notify',
|
action: 'notify',
|
||||||
data: {
|
data: {
|
||||||
description: 'Dunak is nerd',
|
title: 'Success',
|
||||||
title: 'Dunak',
|
description: 'Notification description',
|
||||||
id: 1,
|
type: 'success',
|
||||||
|
id: 'pogchamp',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
debugData<NotificationProps>([
|
||||||
|
|
||||||
export const debugCustomNotification = () => {
|
|
||||||
debugData<CustomNotificationProps>([
|
|
||||||
{
|
{
|
||||||
action: 'customNotify',
|
action: 'notify',
|
||||||
data: {
|
data: {
|
||||||
description: 'Dunak is nerd',
|
title: 'Success',
|
||||||
icon: 'basket-shopping',
|
description: 'Notification description',
|
||||||
style: {
|
type: 'success',
|
||||||
backgroundColor: '#2D3748',
|
id: 'pogchamp',
|
||||||
color: 'white',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
]);
|
||||||
|
debugData<NotificationProps>([
|
||||||
|
{
|
||||||
|
action: 'notify',
|
||||||
|
data: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Notification description',
|
||||||
|
type: 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
debugData<NotificationProps>([
|
||||||
|
{
|
||||||
|
action: 'notify',
|
||||||
|
data: {
|
||||||
|
title: 'Custom icon success',
|
||||||
|
description: 'Notification description',
|
||||||
|
type: 'success',
|
||||||
|
icon: 'microchip',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
import { ProgressbarProps } from '../../progress/Progressbar';
|
import { ProgressbarProps } from '../../../typings';
|
||||||
|
|
||||||
export const debugProgressbar = () => {
|
export const debugProgressbar = () => {
|
||||||
debugData<ProgressbarProps>([
|
debugData<ProgressbarProps>([
|
||||||
|
|||||||
19
web/src/features/dev/debug/radial.ts
Normal file
19
web/src/features/dev/debug/radial.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { debugData } from '../../../utils/debugData';
|
||||||
|
import type { MenuItem } from '../../../typings';
|
||||||
|
|
||||||
|
export const debugRadial = () => {
|
||||||
|
debugData<{ items: MenuItem[]; sub?: boolean }>([
|
||||||
|
{
|
||||||
|
action: 'openRadialMenu',
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{ icon: 'palette', label: 'Paint' },
|
||||||
|
{ icon: 'warehouse', label: 'Garage' },
|
||||||
|
{ icon: 'palette', label: 'Quite long text' },
|
||||||
|
{ icon: 'palette', label: 'Paint' },
|
||||||
|
{ icon: 'warehouse', label: 'Garage' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
import { GameDifficulty } from '../../skillcheck';
|
import { GameDifficulty } from '../../../typings';
|
||||||
|
|
||||||
export const debugSkillCheck = () => {
|
export const debugSkillCheck = () => {
|
||||||
debugData<GameDifficulty | GameDifficulty[]>([
|
debugData<{ difficulty: GameDifficulty | GameDifficulty[]; inputs?: string[] }>([
|
||||||
{
|
{
|
||||||
action: 'startSkillCheck',
|
action: 'startSkillCheck',
|
||||||
data: ['easy', 'easy', 'hard'],
|
data: {
|
||||||
|
difficulty: ['easy', 'easy', 'hard'],
|
||||||
|
inputs: ['W', 'A', 'S', 'D'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TextUiProps } from '../../textui/TextUI';
|
import { TextUiProps } from '../../../typings';
|
||||||
import { debugData } from '../../../utils/debugData';
|
import { debugData } from '../../../utils/debugData';
|
||||||
|
|
||||||
export const debugTextUI = () => {
|
export const debugTextUI = () => {
|
||||||
|
|||||||
@@ -1,91 +1,75 @@
|
|||||||
import {
|
import { ActionIcon, Tooltip, Drawer, Stack, Divider, Button } from '@mantine/core';
|
||||||
Button,
|
|
||||||
Drawer,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerOverlay,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
VStack,
|
|
||||||
Divider,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { debugAlert } from './debug/alert';
|
import { debugAlert } from './debug/alert';
|
||||||
import { debugContext } from './debug/context';
|
import { debugContext } from './debug/context';
|
||||||
import { debugInput } from './debug/input';
|
import { debugInput } from './debug/input';
|
||||||
import { debugMenu } from './debug/menu';
|
import { debugMenu } from './debug/menu';
|
||||||
import { debugCustomNotification, debugNotification } from './debug/notification';
|
import { debugCustomNotification } from './debug/notification';
|
||||||
import { debugCircleProgressbar, debugProgressbar } from './debug/progress';
|
import { debugCircleProgressbar, debugProgressbar } from './debug/progress';
|
||||||
import { debugTextUI } from './debug/textui';
|
import { debugTextUI } from './debug/textui';
|
||||||
import { debugSkillCheck } from './debug/skillcheck';
|
import { debugSkillCheck } from './debug/skillcheck';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { debugRadial } from './debug/radial';
|
||||||
|
|
||||||
const Dev: React.FC = () => {
|
const Dev: React.FC = () => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip label="Developer drawer">
|
<Tooltip label="Developer drawer" position="bottom">
|
||||||
<IconButton
|
<ActionIcon
|
||||||
position="absolute"
|
onClick={() => setOpened(true)}
|
||||||
bottom={0}
|
radius="xl"
|
||||||
right={0}
|
variant="filled"
|
||||||
mr={20}
|
color="orange"
|
||||||
mb={20}
|
sx={{ position: 'absolute', bottom: 0, right: 0, width: 50, height: 50 }}
|
||||||
borderRadius="50%"
|
size="xl"
|
||||||
icon={<FontAwesomeIcon icon="wrench" fixedWidth size="lg" />}
|
mr={50}
|
||||||
colorScheme="orange"
|
mb={50}
|
||||||
size="lg"
|
>
|
||||||
aria-label="Dev tools"
|
<FontAwesomeIcon icon="wrench" fontSize={24} />
|
||||||
onClick={() => onOpen()}
|
</ActionIcon>
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Drawer placement="left" onClose={onClose} isOpen={isOpen}>
|
|
||||||
<DrawerOverlay />
|
<Drawer position="left" onClose={() => setOpened(false)} opened={opened} title="Developer drawer" padding="xl">
|
||||||
<DrawerContent>
|
<Stack>
|
||||||
<DrawerHeader>Developer drawer</DrawerHeader>
|
|
||||||
<DrawerBody>
|
|
||||||
<VStack>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button width="full" onClick={() => debugInput()}>
|
<Button fullWidth onClick={() => debugInput()}>
|
||||||
Open input dialog
|
Open input dialog
|
||||||
</Button>
|
</Button>
|
||||||
<Button width="full" onClick={() => debugAlert()}>
|
<Button fullWidth onClick={() => debugAlert()}>
|
||||||
Open alert dialog
|
Open alert dialog
|
||||||
</Button>
|
</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button width="full" onClick={() => debugContext()}>
|
<Button fullWidth onClick={() => debugContext()}>
|
||||||
Open context menu
|
Open context menu
|
||||||
</Button>
|
</Button>
|
||||||
<Button width="full" onClick={() => debugMenu()}>
|
<Button fullWidth onClick={() => debugMenu()}>
|
||||||
Open list menu
|
Open list menu
|
||||||
</Button>
|
</Button>
|
||||||
<Divider />
|
<Button fullWidth onClick={() => debugRadial()}>
|
||||||
<Button width="full" onClick={() => debugCustomNotification()}>
|
Open radial menu
|
||||||
Send custom notification
|
|
||||||
</Button>
|
|
||||||
<Button width="full" onClick={() => debugNotification()}>
|
|
||||||
Send default notification
|
|
||||||
</Button>
|
</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button width="full" onClick={() => debugProgressbar()}>
|
<Button fullWidth onClick={() => debugCustomNotification()}>
|
||||||
|
Send notification
|
||||||
|
</Button>
|
||||||
|
<Divider />
|
||||||
|
<Button fullWidth onClick={() => debugProgressbar()}>
|
||||||
Activate progress bar
|
Activate progress bar
|
||||||
</Button>
|
</Button>
|
||||||
<Button width="full" onClick={() => debugCircleProgressbar()}>
|
<Button fullWidth onClick={() => debugCircleProgressbar()}>
|
||||||
Activate progress circle
|
Activate progress circle
|
||||||
</Button>
|
</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button width="full" onClick={() => debugTextUI()}>
|
<Button fullWidth onClick={() => debugTextUI()}>
|
||||||
Show TextUI
|
Show TextUI
|
||||||
</Button>
|
</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button width="full" onClick={() => debugSkillCheck()}>
|
<Button fullWidth onClick={() => debugSkillCheck()}>
|
||||||
Run skill check
|
Run skill check
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</Stack>
|
||||||
</DrawerBody>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,83 +1,72 @@
|
|||||||
import {
|
import { Modal, Button, Stack, Group, useMantineTheme } from '@mantine/core';
|
||||||
AlertDialog as Dialog,
|
import { useState } from 'react';
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
useDisclosure,
|
|
||||||
Button,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { fetchNui } from '../../utils/fetchNui';
|
import { fetchNui } from '../../utils/fetchNui';
|
||||||
import { useLocales } from '../../providers/LocaleProvider';
|
import { useLocales } from '../../providers/LocaleProvider';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
export interface AlertProps {
|
import type { AlertProps } from '../../typings';
|
||||||
header: string;
|
|
||||||
content: string;
|
|
||||||
centered?: boolean;
|
|
||||||
cancel?: boolean;
|
|
||||||
labels?: {
|
|
||||||
cancel?: string;
|
|
||||||
confirm?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlertDialog: React.FC = () => {
|
const AlertDialog: React.FC = () => {
|
||||||
const { locale } = useLocales();
|
const { locale } = useLocales();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const theme = useMantineTheme();
|
||||||
const cancelRef = useRef(null);
|
const [opened, setOpened] = useState(false);
|
||||||
const [dialogData, setDialogData] = useState<AlertProps>({
|
const [dialogData, setDialogData] = useState<AlertProps>({
|
||||||
header: '',
|
header: '',
|
||||||
content: '',
|
content: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeAlert = (button: string) => {
|
const closeAlert = (button: string) => {
|
||||||
onClose();
|
setOpened(false);
|
||||||
fetchNui('closeAlert', button);
|
fetchNui('closeAlert', button);
|
||||||
};
|
};
|
||||||
|
|
||||||
useNuiEvent('sendAlert', (data: AlertProps) => {
|
useNuiEvent('sendAlert', (data: AlertProps) => {
|
||||||
setDialogData(data);
|
setDialogData(data);
|
||||||
onOpen();
|
setOpened(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useNuiEvent('closeAlertDialog', () => {
|
useNuiEvent('closeAlertDialog', () => {
|
||||||
onClose();
|
setOpened(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Modal
|
||||||
leastDestructiveRef={cancelRef}
|
opened={opened}
|
||||||
onClose={onClose}
|
centered={dialogData.centered}
|
||||||
isOpen={isOpen}
|
size={dialogData.size || 'md'}
|
||||||
isCentered={dialogData.centered}
|
overflow={dialogData.overflow ? 'inside' : 'outside'}
|
||||||
closeOnOverlayClick={false}
|
closeOnClickOutside={false}
|
||||||
onEsc={() => closeAlert('cancel')}
|
onClose={() => {
|
||||||
|
setOpened(false);
|
||||||
|
closeAlert('cancel');
|
||||||
|
}}
|
||||||
|
withCloseButton={false}
|
||||||
|
overlayOpacity={0.5}
|
||||||
|
exitTransitionDuration={150}
|
||||||
|
transition="fade"
|
||||||
|
title={<ReactMarkdown>{dialogData.header}</ReactMarkdown>}
|
||||||
>
|
>
|
||||||
<AlertDialogOverlay />
|
<Stack>
|
||||||
<AlertDialogContent fontFamily="Inter">
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{dialogData.content}</ReactMarkdown>
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
<Group position="right" spacing={10}>
|
||||||
{dialogData.header}
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogBody>
|
|
||||||
<ReactMarkdown>{dialogData.content}</ReactMarkdown>
|
|
||||||
</AlertDialogBody>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
{dialogData.cancel && (
|
{dialogData.cancel && (
|
||||||
<Button onClick={() => closeAlert('cancel')} mr={3}>
|
<Button uppercase variant="default" onClick={() => closeAlert('cancel')} mr={3}>
|
||||||
{dialogData.labels?.cancel || locale.ui.cancel}
|
{dialogData.labels?.cancel || locale.ui.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button colorScheme={dialogData.cancel ? 'blue' : undefined} onClick={() => closeAlert('confirm')}>
|
<Button
|
||||||
|
uppercase
|
||||||
|
variant={dialogData.cancel ? 'light' : 'default'}
|
||||||
|
color={dialogData.cancel ? theme.primaryColor : undefined}
|
||||||
|
onClick={() => closeAlert('confirm')}
|
||||||
|
>
|
||||||
{dialogData.labels?.confirm || locale.ui.confirm}
|
{dialogData.labels?.confirm || locale.ui.confirm}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</Group>
|
||||||
</AlertDialogContent>
|
</Stack>
|
||||||
</Dialog>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,103 +1,165 @@
|
|||||||
import { Modal, ModalOverlay, ModalContent, ModalFooter, ModalHeader, ModalBody, Button } from '@chakra-ui/react';
|
import { Group, Modal, Button, Stack } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { useLocales } from '../../providers/LocaleProvider';
|
import { useLocales } from '../../providers/LocaleProvider';
|
||||||
import { fetchNui } from '../../utils/fetchNui';
|
import { fetchNui } from '../../utils/fetchNui';
|
||||||
import { IInput, ICheckbox, ISelect, INumber, ISlider } from '../../interfaces/dialog';
|
import { OptionValue } from '../../typings';
|
||||||
import InputField from './components/fields/input';
|
import InputField from './components/fields/input';
|
||||||
import CheckboxField from './components/fields/checkbox';
|
import CheckboxField from './components/fields/checkbox';
|
||||||
import SelectField from './components/fields/select';
|
import SelectField from './components/fields/select';
|
||||||
import NumberField from './components/fields/number';
|
import NumberField from './components/fields/number';
|
||||||
import SliderField from './components/fields/slider';
|
import SliderField from './components/fields/slider';
|
||||||
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import ColorField from './components/fields/color';
|
||||||
|
import DateField from './components/fields/date';
|
||||||
|
import TextareaField from './components/fields/textarea';
|
||||||
|
import TimeField from './components/fields/time';
|
||||||
|
import type { InputProps } from '../../typings';
|
||||||
|
|
||||||
export interface InputProps {
|
export type FormValues = {
|
||||||
heading: string;
|
test: {
|
||||||
rows: Array<IInput | ICheckbox | ISelect | INumber | ISlider>;
|
value: any;
|
||||||
}
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
const InputDialog: React.FC = () => {
|
const InputDialog: React.FC = () => {
|
||||||
const [fields, setFields] = React.useState<InputProps>({
|
const [fields, setFields] = React.useState<InputProps>({
|
||||||
heading: '',
|
heading: '',
|
||||||
rows: [{ type: 'input', label: '' }],
|
rows: [{ type: 'input', label: '' }],
|
||||||
});
|
});
|
||||||
const [inputData, setInputData] = React.useState<Array<string | number | boolean>>([]);
|
|
||||||
const [passwordStates, setPasswordStates] = React.useState<boolean[]>([]);
|
|
||||||
const [visible, setVisible] = React.useState(false);
|
const [visible, setVisible] = React.useState(false);
|
||||||
const { locale } = useLocales();
|
const { locale } = useLocales();
|
||||||
|
|
||||||
const handlePasswordStates = (index: number) => {
|
const form = useForm<{ test: { value: any }[] }>({});
|
||||||
setPasswordStates({
|
const fieldForm = useFieldArray({
|
||||||
...passwordStates,
|
control: form.control,
|
||||||
[index]: !passwordStates[index],
|
name: 'test',
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
useNuiEvent<InputProps>('openDialog', (data) => {
|
useNuiEvent<InputProps>('openDialog', (data) => {
|
||||||
setPasswordStates([]);
|
|
||||||
setFields(data);
|
setFields(data);
|
||||||
setInputData([]);
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
data.rows.forEach((row, index) => {
|
||||||
|
fieldForm.insert(
|
||||||
|
index,
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
row.type !== 'checkbox'
|
||||||
|
? row.type === 'date' || row.type === 'date-range' || row.type === 'time'
|
||||||
|
? // Set date to current one if default is set to true
|
||||||
|
row.default === true
|
||||||
|
? new Date().getTime()
|
||||||
|
: Array.isArray(row.default)
|
||||||
|
? row.default.map((date) => new Date(date).getTime())
|
||||||
|
: row.default && new Date(row.default).getTime()
|
||||||
|
: row.default
|
||||||
|
: row.checked,
|
||||||
|
} || { value: null }
|
||||||
|
);
|
||||||
|
// Backwards compat with new Select data type
|
||||||
|
if (row.type === 'select' || row.type === 'multi-select') {
|
||||||
|
row.options = row.options.map((option) =>
|
||||||
|
!option.label ? { ...option, label: option.value } : option
|
||||||
|
) as Array<OptionValue>;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useNuiEvent('closeInputDialog', () => {
|
useNuiEvent('closeInputDialog', () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = async () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
fetchNui('inputData');
|
fetchNui('inputData');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
form.reset();
|
||||||
|
fieldForm.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (value: string | number | boolean, index: number) => {
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
setInputData((previousData) => {
|
|
||||||
previousData[index] = value;
|
|
||||||
return previousData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
fetchNui('inputData', inputData);
|
const values: any[] = [];
|
||||||
};
|
Object.values(data.test).forEach((obj: { value: any }) => values.push(obj.value));
|
||||||
|
console.log(values);
|
||||||
|
fetchNui('inputData', values);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
form.reset();
|
||||||
|
fieldForm.remove();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={visible} onClose={handleClose} isCentered closeOnEsc closeOnOverlayClick={false} size="xs">
|
<Modal
|
||||||
<ModalOverlay />
|
opened={visible}
|
||||||
<ModalContent
|
onClose={handleClose}
|
||||||
onKeyDown={(e) => {
|
centered
|
||||||
if (e.key === 'Enter' && visible) return handleConfirm();
|
closeOnEscape={fields.options?.allowCancel !== false}
|
||||||
}}
|
closeOnClickOutside={false}
|
||||||
|
size="xs"
|
||||||
|
styles={{ title: { textAlign: 'center', width: '100%', fontSize: 18 } }}
|
||||||
|
title={fields.heading}
|
||||||
|
withCloseButton={false}
|
||||||
|
overlayOpacity={0.5}
|
||||||
|
transition="fade"
|
||||||
|
exitTransitionDuration={150}
|
||||||
>
|
>
|
||||||
<ModalHeader textAlign="center">{fields.heading}</ModalHeader>
|
<form onSubmit={onSubmit}>
|
||||||
<ModalBody fontFamily="Poppins" textAlign="left">
|
<Stack>
|
||||||
{fields.rows.map((row: IInput | ICheckbox | ISelect | INumber | ISlider, index) => (
|
{fieldForm.fields.map((item, index) => {
|
||||||
<React.Fragment key={`row-${index}-${row.type}-${row.label}`}>
|
const row = fields.rows[index];
|
||||||
|
return (
|
||||||
|
<React.Fragment key={item.id}>
|
||||||
{row.type === 'input' && (
|
{row.type === 'input' && (
|
||||||
<InputField
|
<InputField
|
||||||
|
register={form.register(`test.${index}.value`, { required: row.required })}
|
||||||
|
row={row}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{row.type === 'checkbox' && (
|
||||||
|
<CheckboxField
|
||||||
|
register={form.register(`test.${index}.value`, { required: row.required })}
|
||||||
|
row={row}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(row.type === 'select' || row.type === 'multi-select') && (
|
||||||
|
<SelectField row={row} index={index} control={form.control} />
|
||||||
|
)}
|
||||||
|
{row.type === 'number' && <NumberField control={form.control} row={row} index={index} />}
|
||||||
|
{row.type === 'slider' && <SliderField control={form.control} row={row} index={index} />}
|
||||||
|
{row.type === 'color' && <ColorField control={form.control} row={row} index={index} />}
|
||||||
|
{row.type === 'time' && <TimeField control={form.control} row={row} index={index} />}
|
||||||
|
{row.type === 'date' || row.type === 'date-range' ? (
|
||||||
|
<DateField control={form.control} row={row} index={index} />
|
||||||
|
) : null}
|
||||||
|
{row.type === 'textarea' && (
|
||||||
|
<TextareaField
|
||||||
|
register={form.register(`test.${index}.value`, { required: row.required })}
|
||||||
row={row}
|
row={row}
|
||||||
index={index}
|
index={index}
|
||||||
handleChange={handleChange}
|
|
||||||
passwordStates={passwordStates}
|
|
||||||
handlePasswordStates={handlePasswordStates}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{row.type === 'checkbox' && <CheckboxField row={row} index={index} handleChange={handleChange} />}
|
|
||||||
{row.type === 'select' && <SelectField row={row} index={index} handleChange={handleChange} />}
|
|
||||||
{row.type === 'number' && <NumberField row={row} index={index} handleChange={handleChange} />}
|
|
||||||
{row.type === 'slider' && <SliderField row={row} index={index} handleChange={handleChange} />}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
);
|
||||||
</ModalBody>
|
})}
|
||||||
<ModalFooter>
|
<Group position="right" spacing={10}>
|
||||||
<Button mr={3} onClick={handleClose}>
|
<Button
|
||||||
{locale.ui.close}
|
uppercase
|
||||||
|
variant="default"
|
||||||
|
onClick={handleClose}
|
||||||
|
mr={3}
|
||||||
|
disabled={fields.options?.allowCancel === false}
|
||||||
|
>
|
||||||
|
{locale.ui.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorScheme="blue" onClick={handleConfirm}>
|
<Button uppercase variant="light" type="submit">
|
||||||
{locale.ui.confirm}
|
{locale.ui.confirm}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Group>
|
||||||
</ModalContent>
|
</Stack>
|
||||||
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Box, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
const InfoTooltip: React.FC<{ description: string }> = ({ description }) => {
|
|
||||||
return (
|
|
||||||
<Tooltip label={description} placement="top" hasArrow arrowSize={4} maxW={220}>
|
|
||||||
<Box
|
|
||||||
borderRadius="full"
|
|
||||||
bg="whiteAlpha.200"
|
|
||||||
p={0.5}
|
|
||||||
w={18}
|
|
||||||
h={18}
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon="question" fixedWidth fontSize={10} />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InfoTooltip;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { HStack, Text } from '@chakra-ui/react';
|
|
||||||
import InfoTooltip from './InfoTooltip';
|
|
||||||
|
|
||||||
const Label: React.FC<{ label: string; description?: string }> = ({ label, description }) => {
|
|
||||||
return (
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Text>{label}</Text>
|
|
||||||
{description && <InfoTooltip description={description} />}
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Label;
|
|
||||||
@@ -1,30 +1,22 @@
|
|||||||
import { Box, Checkbox, HStack, Text } from '@chakra-ui/react';
|
import { Checkbox } from '@mantine/core';
|
||||||
import { useEffect } from 'react';
|
import { ICheckbox } from '../../../../typings/dialog';
|
||||||
import { ICheckbox } from '../../../../interfaces/dialog';
|
import { UseFormRegisterReturn } from 'react-hook-form';
|
||||||
import Label from '../Label';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: ICheckbox;
|
row: ICheckbox;
|
||||||
index: number;
|
index: number;
|
||||||
handleChange: (value: boolean, index: number) => void;
|
register: UseFormRegisterReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxField: React.FC<Props> = (props) => {
|
const CheckboxField: React.FC<Props> = (props) => {
|
||||||
useEffect(() => {
|
|
||||||
if (props.row.checked) props.handleChange(props.row.checked, props.index);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Box mb={3}>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => props.handleChange(e.target.checked, props.index)}
|
{...props.register}
|
||||||
|
sx={{ display: 'flex' }}
|
||||||
|
required={props.row.required}
|
||||||
|
label={props.row.label}
|
||||||
defaultChecked={props.row.checked}
|
defaultChecked={props.row.checked}
|
||||||
>
|
/>
|
||||||
<Label label={props.row.label} description={props.row.description} />
|
|
||||||
</Checkbox>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
40
web/src/features/dialog/components/fields/color.tsx
Normal file
40
web/src/features/dialog/components/fields/color.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { IColorInput } from '../../../../typings/dialog';
|
||||||
|
import { Control, useController } from 'react-hook-form';
|
||||||
|
import { FormValues } from '../../InputDialog';
|
||||||
|
import { ColorInput } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: IColorInput;
|
||||||
|
index: number;
|
||||||
|
control: Control<FormValues>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColorField: React.FC<Props> = (props) => {
|
||||||
|
const controller = useController({
|
||||||
|
name: `test.${props.index}.value`,
|
||||||
|
control: props.control,
|
||||||
|
defaultValue: props.row.default,
|
||||||
|
rules: { required: props.row.required },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorInput
|
||||||
|
withEyeDropper={false}
|
||||||
|
value={controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={controller.field.onChange}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
defaultValue={props.row.default}
|
||||||
|
format={props.row.format}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorField;
|
||||||
73
web/src/features/dialog/components/fields/date.tsx
Normal file
73
web/src/features/dialog/components/fields/date.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { IDateInput } from '../../../../typings/dialog';
|
||||||
|
import { Control, useController } from 'react-hook-form';
|
||||||
|
import { FormValues } from '../../InputDialog';
|
||||||
|
import { DatePicker, DateRangePicker, TimeInput } from '@mantine/dates';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: IDateInput;
|
||||||
|
index: number;
|
||||||
|
control: Control<FormValues>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateField: React.FC<Props> = (props) => {
|
||||||
|
const controller = useController({
|
||||||
|
name: `test.${props.index}.value`,
|
||||||
|
control: props.control,
|
||||||
|
rules: { required: props.row.required },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.row.type === 'date' && (
|
||||||
|
<DatePicker
|
||||||
|
value={controller.field.value ? new Date(controller.field.value) : controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
// Workaround to use timestamp instead of Date object in values
|
||||||
|
onChange={(date) => controller.field.onChange(date ? date.getTime() : null)}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
placeholder={props.row.format}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
inputFormat={props.row.format}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
clearable={props.row.clearable}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon fixedWidth icon={props.row.icon} />}
|
||||||
|
minDate={props.row.min ? new Date(props.row.min) : undefined}
|
||||||
|
maxDate={props.row.max ? new Date(props.row.max) : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.row.type === 'date-range' && (
|
||||||
|
<DateRangePicker
|
||||||
|
value={
|
||||||
|
controller.field.value
|
||||||
|
? controller.field.value[0]
|
||||||
|
? controller.field.value.map((date: Date) => new Date(date))
|
||||||
|
: controller.field.value
|
||||||
|
: controller.field.value
|
||||||
|
}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={(dates) =>
|
||||||
|
controller.field.onChange(dates.map((date: Date | null) => (date ? date.getTime() : null)))
|
||||||
|
}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
placeholder={props.row.format}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
inputFormat={props.row.format}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
clearable={props.row.clearable}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon fixedWidth icon={props.row.icon} />}
|
||||||
|
minDate={props.row.min ? new Date(props.row.min) : undefined}
|
||||||
|
maxDate={props.row.max ? new Date(props.row.max) : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateField;
|
||||||
@@ -1,53 +1,58 @@
|
|||||||
import { Box, InputGroup, InputLeftElement, InputRightElement, Input } from '@chakra-ui/react';
|
import { createStyles, PasswordInput, TextInput } from '@mantine/core';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { IInput } from '../../../../interfaces/dialog';
|
import { IInput } from '../../../../typings/dialog';
|
||||||
import Label from '../Label';
|
import { UseFormRegisterReturn } from 'react-hook-form';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
register: UseFormRegisterReturn;
|
||||||
row: IInput;
|
row: IInput;
|
||||||
index: number;
|
index: number;
|
||||||
handleChange: (value: string, index: number) => void;
|
|
||||||
passwordStates: boolean[];
|
|
||||||
handlePasswordStates: (index: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
eyeIcon: {
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const InputField: React.FC<Props> = (props) => {
|
const InputField: React.FC<Props> = (props) => {
|
||||||
useEffect(() => {
|
const { classes } = useStyles();
|
||||||
if (props.row.default) props.handleChange(props.row.default, props.index);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={3} textAlign="left">
|
{!props.row.password ? (
|
||||||
<Label label={props.row.label} description={props.row.description} />
|
<TextInput
|
||||||
<InputGroup>
|
{...props.register}
|
||||||
{props.row.icon && (
|
|
||||||
<InputLeftElement pointerEvents="none" children={<FontAwesomeIcon icon={props.row.icon} fixedWidth />} />
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => props.handleChange(e.target.value, props.index)}
|
|
||||||
placeholder={props.row.placeholder}
|
|
||||||
defaultValue={props.row.default}
|
defaultValue={props.row.default}
|
||||||
type={!props.row.password || props.passwordStates[props.index] ? 'text' : 'password'}
|
label={props.row.label}
|
||||||
isDisabled={props.row.disabled}
|
description={props.row.description}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
placeholder={props.row.placeholder}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
/>
|
/>
|
||||||
{props.row.password && (
|
) : (
|
||||||
<InputRightElement
|
<PasswordInput
|
||||||
cursor="pointer"
|
{...props.register}
|
||||||
onClick={() => props.handlePasswordStates(props.index)}
|
defaultValue={props.row.default}
|
||||||
children={
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
placeholder={props.row.placeholder}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
visibilityToggleIcon={({ reveal, size }) => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
icon={reveal ? 'eye-slash' : 'eye'}
|
||||||
|
fontSize={size}
|
||||||
|
cursor="pointer"
|
||||||
|
className={classes.eyeIcon}
|
||||||
fixedWidth
|
fixedWidth
|
||||||
icon={props.passwordStates[props.index] ? 'eye' : 'eye-slash'}
|
|
||||||
fontSize="1em"
|
|
||||||
style={{ paddingRight: 8 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InputGroup>
|
/>
|
||||||
</Box>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,39 @@
|
|||||||
import {
|
import { NumberInput } from '@mantine/core';
|
||||||
Box,
|
import { INumber } from '../../../../typings/dialog';
|
||||||
NumberInput,
|
|
||||||
NumberInputField,
|
|
||||||
NumberInputStepper,
|
|
||||||
NumberIncrementStepper,
|
|
||||||
NumberDecrementStepper,
|
|
||||||
InputLeftElement,
|
|
||||||
InputGroup,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { INumber } from '../../../../interfaces/dialog';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import Label from '../Label';
|
import { Control, useController } from 'react-hook-form';
|
||||||
|
import { FormValues } from '../../InputDialog';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: INumber;
|
row: INumber;
|
||||||
index: number;
|
index: number;
|
||||||
handleChange: (value: number, index: number) => void;
|
control: Control<FormValues>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumberField: React.FC<Props> = (props) => {
|
const NumberField: React.FC<Props> = (props) => {
|
||||||
useEffect(() => {
|
const controller = useController({
|
||||||
if (props.row.default) props.handleChange(props.row.default, props.index);
|
name: `test.${props.index}.value`,
|
||||||
}, []);
|
control: props.control,
|
||||||
|
defaultValue: props.row.default,
|
||||||
|
rules: { required: props.row.required },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb={3}>
|
|
||||||
<Label label={props.row.label} description={props.row.description} />
|
|
||||||
<InputGroup>
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
onChange={(val: string) => props.handleChange(+val, props.index)}
|
value={controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={controller.field.onChange}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
defaultValue={props.row.default}
|
defaultValue={props.row.default}
|
||||||
min={props.row.min}
|
min={props.row.min}
|
||||||
max={props.row.max}
|
max={props.row.max}
|
||||||
isDisabled={props.row.disabled}
|
disabled={props.row.disabled}
|
||||||
w="100%"
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
>
|
withAsterisk={props.row.required}
|
||||||
{props.row.icon && (
|
/>
|
||||||
<InputLeftElement pointerEvents="none" children={<FontAwesomeIcon icon={props.row.icon} fixedWidth />} />
|
|
||||||
)}
|
|
||||||
<NumberInputField placeholder={props.row.placeholder} pl={props.row.icon ? '40px' : undefined} />
|
|
||||||
<NumberInputStepper>
|
|
||||||
<NumberIncrementStepper />
|
|
||||||
<NumberDecrementStepper />
|
|
||||||
</NumberInputStepper>
|
|
||||||
</NumberInput>
|
|
||||||
</InputGroup>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,60 @@
|
|||||||
import { Box, Select } from '@chakra-ui/react';
|
import { MultiSelect, Select } from '@mantine/core';
|
||||||
import { useEffect } from 'react';
|
import { ISelect } from '../../../../typings/dialog';
|
||||||
import { ISelect } from '../../../../interfaces/dialog';
|
import { Control, useController } from 'react-hook-form';
|
||||||
|
import { FormValues } from '../../InputDialog';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: ISelect;
|
row: ISelect;
|
||||||
index: number;
|
index: number;
|
||||||
handleChange: (value: string, index: number) => void;
|
control: Control<FormValues>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectField: React.FC<Props> = (props) => {
|
const SelectField: React.FC<Props> = (props) => {
|
||||||
useEffect(() => {
|
const controller = useController({
|
||||||
if (props.row.default) {
|
name: `test.${props.index}.value`,
|
||||||
props.row.options?.map((option) => {
|
control: props.control,
|
||||||
if (props.row.default === option.value) {
|
defaultValue: props.row.default || props.row.type !== 'multi-select' ? props.row.options[0].value : undefined,
|
||||||
props.handleChange(option.value, props.index);
|
rules: { required: props.row.required },
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={3}>
|
{props.row.type === 'select' ? (
|
||||||
<Select
|
<Select
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => props.handleChange(e.target.value, props.index)}
|
data={props.row.options}
|
||||||
defaultValue={props.row.default || ''}
|
value={controller.field.value}
|
||||||
isDisabled={props.row.disabled}
|
name={controller.field.name}
|
||||||
>
|
ref={controller.field.ref}
|
||||||
{/* Hacky workaround for selectable placeholder issue */}
|
onBlur={controller.field.onBlur}
|
||||||
{!props.row.default && (
|
onChange={controller.field.onChange}
|
||||||
<option value="" hidden disabled>
|
disabled={props.row.disabled}
|
||||||
{props.row.label}
|
label={props.row.label}
|
||||||
</option>
|
description={props.row.description}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
clearable={props.row.clearable}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{props.row.type === 'multi-select' && (
|
||||||
|
<MultiSelect
|
||||||
|
data={props.row.options}
|
||||||
|
value={controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={controller.field.onChange}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
clearable={props.row.clearable}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{props.row.options?.map((option, index) => (
|
|
||||||
<option key={`option-${index}`} value={option.value}>
|
|
||||||
{option.label || option.value}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +1,42 @@
|
|||||||
import { Box, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, HStack, Tooltip } from '@chakra-ui/react';
|
import { Box, Slider, Text } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { ISlider } from '../../../../typings/dialog';
|
||||||
import { ISlider } from '../../../../interfaces/dialog';
|
import { Control, useController } from 'react-hook-form';
|
||||||
import Label from '../Label';
|
import { FormValues } from '../../InputDialog';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: ISlider;
|
row: ISlider;
|
||||||
index: number;
|
index: number;
|
||||||
handleChange: (value: number, index: number) => void;
|
control: Control<FormValues>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SliderField: React.FC<Props> = (props) => {
|
const SliderField: React.FC<Props> = (props) => {
|
||||||
useEffect(() => {
|
const controller = useController({
|
||||||
if (props.row.default || props.row.min) props.handleChange(props.row.default || props.row.min!, props.index);
|
name: `test.${props.index}.value`,
|
||||||
}, []);
|
control: props.control,
|
||||||
|
defaultValue: props.row.default || props.row.min || 0,
|
||||||
const [sliderValue, setSliderValue] = useState(props.row.default || props.row.min || 0);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<Box mb={3}>
|
<Text sx={{ fontSize: 14, fontWeight: 500 }}>{props.row.label}</Text>
|
||||||
<Label label={props.row.label} description={props.row.description} />
|
|
||||||
<Slider
|
<Slider
|
||||||
onChangeEnd={(val: number) => props.handleChange(val, props.index)}
|
mb={10}
|
||||||
onChange={(val: number) => setSliderValue(val)}
|
value={controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={controller.field.onChange}
|
||||||
defaultValue={props.row.default || props.row.min || 0}
|
defaultValue={props.row.default || props.row.min || 0}
|
||||||
min={props.row.min}
|
min={props.row.min}
|
||||||
max={props.row.max}
|
max={props.row.max}
|
||||||
step={props.row.step}
|
step={props.row.step}
|
||||||
isDisabled={props.row.disabled}
|
disabled={props.row.disabled}
|
||||||
>
|
marks={[
|
||||||
<SliderTrack>
|
{ value: props.row.min || 0, label: props.row.min || 0 },
|
||||||
<SliderFilledTrack />
|
{ value: props.row.max || 100, label: props.row.max || 100 },
|
||||||
</SliderTrack>
|
]}
|
||||||
<Tooltip hasArrow label={sliderValue} placement="bottom" gutter={10}>
|
/>
|
||||||
<SliderThumb />
|
|
||||||
</Tooltip>
|
|
||||||
</Slider>
|
|
||||||
<HStack justifyContent="space-between">
|
|
||||||
<Text fontSize="sm">{props.row.min || 0}</Text>
|
|
||||||
<Text fontSize="sm">{props.row.max || 100}</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
31
web/src/features/dialog/components/fields/textarea.tsx
Normal file
31
web/src/features/dialog/components/fields/textarea.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Textarea } from '@mantine/core';
|
||||||
|
import { UseFormRegisterReturn } from 'react-hook-form';
|
||||||
|
import { ITextarea } from '../../../../typings/dialog';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
register: UseFormRegisterReturn;
|
||||||
|
row: ITextarea;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextareaField: React.FC<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...props.register}
|
||||||
|
defaultValue={props.row.default}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon icon={props.row.icon} fixedWidth />}
|
||||||
|
placeholder={props.row.placeholder}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
autosize={props.row.autosize}
|
||||||
|
minRows={props.row.min}
|
||||||
|
maxRows={props.row.max}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextareaField;
|
||||||
38
web/src/features/dialog/components/fields/time.tsx
Normal file
38
web/src/features/dialog/components/fields/time.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { TimeInput } from '@mantine/dates';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Control, useController } from 'react-hook-form';
|
||||||
|
import { ITimeInput } from '../../../../typings/dialog';
|
||||||
|
import { FormValues } from '../../InputDialog';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
row: ITimeInput;
|
||||||
|
index: number;
|
||||||
|
control: Control<FormValues>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimeField: React.FC<Props> = (props) => {
|
||||||
|
const controller = useController({
|
||||||
|
name: `test.${props.index}.value`,
|
||||||
|
control: props.control,
|
||||||
|
rules: { required: props.row.required },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimeInput
|
||||||
|
value={controller.field.value ? new Date(controller.field.value) : controller.field.value}
|
||||||
|
name={controller.field.name}
|
||||||
|
ref={controller.field.ref}
|
||||||
|
onBlur={controller.field.onBlur}
|
||||||
|
onChange={(date) => controller.field.onChange(date ? date.getTime() : null)}
|
||||||
|
label={props.row.label}
|
||||||
|
description={props.row.description}
|
||||||
|
disabled={props.row.disabled}
|
||||||
|
format={props.row.format || '12'}
|
||||||
|
withAsterisk={props.row.required}
|
||||||
|
clearable={props.row.clearable}
|
||||||
|
icon={props.row.icon && <FontAwesomeIcon fixedWidth icon={props.row.icon} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeField;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
||||||
import { Box, Text, Flex, ScaleFade } from '@chakra-ui/react';
|
import { Box, Stack, Text, Flex } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ContextMenuProps } from '../../../interfaces/context';
|
import { ContextMenuProps } from '../../../typings';
|
||||||
import Item from './Item';
|
import ContextButton from './components/ContextButton';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
import { fetchNui } from '../../../utils/fetchNui';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import HeaderButton from './components/HeaderButton';
|
||||||
|
import ScaleFade from '../../../transitions/ScaleFade';
|
||||||
|
|
||||||
const openMenu = (id: string | undefined) => {
|
const openMenu = (id: string | undefined) => {
|
||||||
fetchNui<ContextMenuProps>('openContext', { id: id, back: true });
|
fetchNui<ContextMenuProps>('openContext', { id: id, back: true });
|
||||||
@@ -49,64 +50,28 @@ const ContextMenu: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="absolute" w="75%" h="80%" justifyContent="flex-end" alignItems="center">
|
<Box sx={{ position: 'absolute', top: '15%', right: '25%' }} w={320} h={580}>
|
||||||
<ScaleFade in={visible} unmountOnExit>
|
<ScaleFade visible={visible}>
|
||||||
<Box w="xs" h={580}>
|
<Flex justify="center" align="center" mb={10} gap={6}>
|
||||||
<Flex justifyContent="center" alignItems="center" mb={3}>
|
|
||||||
{contextMenu.menu && (
|
{contextMenu.menu && (
|
||||||
<Flex
|
<HeaderButton icon="chevron-left" iconSize={16} handleClick={() => openMenu(contextMenu.menu)} />
|
||||||
borderRadius="md"
|
|
||||||
bg="gray.800"
|
|
||||||
flex="1 15%"
|
|
||||||
alignSelf="stretch"
|
|
||||||
textAlign="center"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
marginRight={2}
|
|
||||||
p={2}
|
|
||||||
_hover={{ bg: 'gray.700' }}
|
|
||||||
transition="300ms"
|
|
||||||
onClick={() => openMenu(contextMenu.menu)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon="chevron-left" />
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
<Box borderRadius="md" bg="gray.800" flex="1 85%">
|
<Box sx={{ borderRadius: 4, flex: '1 85%' }} bg="dark.6">
|
||||||
<Text fontFamily="Poppins" fontSize="md" p={2} textAlign="center" fontWeight="light">
|
<Text color="dark.0" p={6} align="center">
|
||||||
<ReactMarkdown>{contextMenu.title}</ReactMarkdown>
|
<ReactMarkdown>{contextMenu.title}</ReactMarkdown>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex
|
<HeaderButton icon="xmark" canClose={contextMenu.canClose} iconSize={18} handleClick={closeContext} />
|
||||||
borderRadius="md"
|
|
||||||
as="button"
|
|
||||||
bg={contextMenu.canClose === false ? 'gray.600' : 'gray.800'}
|
|
||||||
flex="1 15%"
|
|
||||||
alignSelf="stretch"
|
|
||||||
textAlign="center"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
marginLeft={2}
|
|
||||||
p={2}
|
|
||||||
cursor={contextMenu.canClose === false ? 'not-allowed' : undefined}
|
|
||||||
_hover={{ bg: contextMenu.canClose === false ? undefined : 'gray.700' }}
|
|
||||||
transition="300ms"
|
|
||||||
onClick={() => closeContext()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="xmark"
|
|
||||||
fontSize={20}
|
|
||||||
color={contextMenu.canClose === false ? '#718096' : undefined}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
<Box sx={{ height: 560, overflowY: 'scroll' }}>
|
||||||
<Box maxH={560} overflowY="scroll">
|
<Stack spacing={3}>
|
||||||
{Object.entries(contextMenu.options).map((option, index) => (
|
{Object.entries(contextMenu.options).map((option, index) => (
|
||||||
<Item option={option} key={`context-item-${index}`} />
|
<ContextButton option={option} key={`context-item-${index}`} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</ScaleFade>
|
</ScaleFade>
|
||||||
</Flex>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import {
|
|
||||||
Portal,
|
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverBody,
|
|
||||||
PopoverContent,
|
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Flex,
|
|
||||||
Spacer,
|
|
||||||
Image,
|
|
||||||
HStack,
|
|
||||||
Progress,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import { Option, ContextMenuProps } from '../../../interfaces/context';
|
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
|
||||||
|
|
||||||
const openMenu = (id: string | undefined) => {
|
|
||||||
fetchNui<ContextMenuProps>('openContext', { id: id, back: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContext = (id: string) => {
|
|
||||||
fetchNui('clickContext', id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item: React.FC<{
|
|
||||||
option: [string, Option];
|
|
||||||
}> = ({ option }) => {
|
|
||||||
const button = option[1];
|
|
||||||
const buttonKey = option[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Popover placement="right-start" trigger="hover" eventListeners={{ scroll: true }} isLazy>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<Box
|
|
||||||
bg={button.disabled ? 'gray.600' : 'gray.800'}
|
|
||||||
borderRadius="md"
|
|
||||||
h="fit-content"
|
|
||||||
w="100%"
|
|
||||||
p={2}
|
|
||||||
mb={1}
|
|
||||||
fontFamily="Poppins"
|
|
||||||
fontSize="md"
|
|
||||||
transition="300ms"
|
|
||||||
_hover={{ bg: !button.disabled && 'gray.700' }}
|
|
||||||
cursor={(button.disabled && 'not-allowed') || undefined}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
w="100%"
|
|
||||||
alignItems="center"
|
|
||||||
color={button.disabled ? '#718096' : undefined}
|
|
||||||
onClick={() =>
|
|
||||||
!button.disabled ? (button.menu ? openMenu(button.menu) : clickContext(buttonKey)) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{button?.icon && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
fixedWidth
|
|
||||||
icon={button.icon}
|
|
||||||
fontSize={20}
|
|
||||||
style={{
|
|
||||||
marginRight: 10,
|
|
||||||
justifySelf: 'center',
|
|
||||||
color: button.iconColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box w="100%">
|
|
||||||
<Box>
|
|
||||||
<Text w="100%" fontWeight="medium" color={button.disabled ? '#718096' : undefined}>
|
|
||||||
<ReactMarkdown>{button.title ? button.title : buttonKey}</ReactMarkdown>
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{button.description && (
|
|
||||||
<Box paddingBottom={1} color={button.disabled ? '#718096' : undefined}>
|
|
||||||
<Text><ReactMarkdown>{button.description}</ReactMarkdown></Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{button?.progress && (
|
|
||||||
<Progress
|
|
||||||
value={button.progress}
|
|
||||||
size="sm"
|
|
||||||
colorScheme={button.colorScheme || 'gray'}
|
|
||||||
borderRadius="md"
|
|
||||||
marginRight="5px"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{(button.menu || button.arrow) && button.arrow !== false && (
|
|
||||||
<>
|
|
||||||
<Spacer />
|
|
||||||
<Box alignSelf="center" justifySelf="center" mr={4} fontSize="xl">
|
|
||||||
<FontAwesomeIcon icon="chevron-right" />
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Portal>
|
|
||||||
{!button.disabled && (button.metadata || button.image) && (
|
|
||||||
<PopoverContent
|
|
||||||
fontFamily="Poppins"
|
|
||||||
bg="gray.800"
|
|
||||||
outline="none"
|
|
||||||
border="none"
|
|
||||||
w="fit-content"
|
|
||||||
maxW="2xs"
|
|
||||||
>
|
|
||||||
<PopoverBody>
|
|
||||||
<>
|
|
||||||
{button.image && <Image src={button.image} />}
|
|
||||||
{Array.isArray(button.metadata) ? (
|
|
||||||
button.metadata.map((metadata: string | { label: string; value: any }, index: number) => (
|
|
||||||
<Text key={`context-metadata-${index}`}>
|
|
||||||
{typeof metadata === 'string' ? `${metadata}` : `${metadata.label}: ${metadata.value}`}
|
|
||||||
</Text>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{typeof button.metadata === 'object' &&
|
|
||||||
Object.entries(button.metadata).map((metadata: { [key: string]: any }, index) => (
|
|
||||||
<Text key={`context-metadata-${index}`}>
|
|
||||||
{metadata[0]}: {metadata[1]}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
)}
|
|
||||||
</Portal>
|
|
||||||
</Box>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Item;
|
|
||||||
122
web/src/features/menu/context/components/ContextButton.tsx
Normal file
122
web/src/features/menu/context/components/ContextButton.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { Button, Box, Group, Stack, Text, Progress, HoverCard, Image, createStyles } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Option, ContextMenuProps } from '../../../../typings';
|
||||||
|
import { fetchNui } from '../../../../utils/fetchNui';
|
||||||
|
|
||||||
|
const openMenu = (id: string | undefined) => {
|
||||||
|
fetchNui<ContextMenuProps>('openContext', { id: id, back: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickContext = (id: string) => {
|
||||||
|
fetchNui('clickContext', id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, params: { disabled?: boolean }) => ({
|
||||||
|
inner: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
width: '100%',
|
||||||
|
color: params.disabled ? theme.colors.dark[3] : theme.colors.dark[0],
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
color: params.disabled ? theme.colors.dark[3] : theme.colors.dark[2],
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
padding: 10,
|
||||||
|
color: theme.colors.dark[0],
|
||||||
|
fontSize: 14,
|
||||||
|
maxWidth: 256,
|
||||||
|
width: 'fit-content',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ContextButton: React.FC<{
|
||||||
|
option: [string, Option];
|
||||||
|
}> = ({ option }) => {
|
||||||
|
const button = option[1];
|
||||||
|
const buttonKey = option[0];
|
||||||
|
const { classes } = useStyles({ disabled: button.disabled });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HoverCard
|
||||||
|
position="right-start"
|
||||||
|
disabled={button.disabled || !(button.metadata || button.image)}
|
||||||
|
openDelay={200}
|
||||||
|
>
|
||||||
|
<HoverCard.Target>
|
||||||
|
<Button
|
||||||
|
classNames={{ inner: classes.inner, label: classes.label }}
|
||||||
|
onClick={() => (!button.disabled ? (button.menu ? openMenu(button.menu) : clickContext(buttonKey)) : null)}
|
||||||
|
variant="default"
|
||||||
|
h="fit-content"
|
||||||
|
p={10}
|
||||||
|
fullWidth
|
||||||
|
disabled={button.disabled}
|
||||||
|
>
|
||||||
|
<Group position="apart" w="100%" noWrap>
|
||||||
|
<Stack spacing={4} style={{ flex: '1' }}>
|
||||||
|
<Group spacing={8} noWrap>
|
||||||
|
{button?.icon && (
|
||||||
|
<Stack w={25} h={25} justify="center" align="center">
|
||||||
|
<FontAwesomeIcon icon={button.icon} fixedWidth size="lg" style={{ color: button.iconColor }} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Text sx={{ overflowWrap: 'break-word' }}>
|
||||||
|
<ReactMarkdown>{button.title || buttonKey}</ReactMarkdown>
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{button.description && (
|
||||||
|
<Text size={12} className={classes.description}>
|
||||||
|
<ReactMarkdown>{button.description}</ReactMarkdown>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{button.progress !== undefined && (
|
||||||
|
<Progress value={button.progress} size="sm" color={button.colorScheme || 'dark.3'} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{(button.menu || button.arrow) && button.arrow !== false && (
|
||||||
|
<Stack justify="center" w={25} h={25} align="center">
|
||||||
|
<FontAwesomeIcon icon="chevron-right" fixedWidth />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown className={classes.dropdown}>
|
||||||
|
{button.image && <Image src={button.image} />}
|
||||||
|
{Array.isArray(button.metadata) ? (
|
||||||
|
button.metadata.map(
|
||||||
|
(metadata: string | { label: string; value?: any; progress?: number }, index: number) => (
|
||||||
|
<>
|
||||||
|
<Text key={`context-metadata-${index}`}>
|
||||||
|
{typeof metadata === 'string' ? `${metadata}` : `${metadata.label}: ${metadata?.value ?? ''}`}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{typeof metadata === 'object' && metadata.progress !== undefined && (
|
||||||
|
<Progress value={metadata.progress} size="sm" color={button.colorScheme || 'dark.3'} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{typeof button.metadata === 'object' &&
|
||||||
|
Object.entries(button.metadata).map((metadata: { [key: string]: any }, index) => (
|
||||||
|
<Text key={`context-metadata-${index}`}>
|
||||||
|
{metadata[0]}: {metadata[1]}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextButton;
|
||||||
46
web/src/features/menu/context/components/HeaderButton.tsx
Normal file
46
web/src/features/menu/context/components/HeaderButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button, createStyles } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: IconProp;
|
||||||
|
canClose?: boolean;
|
||||||
|
iconSize: number;
|
||||||
|
handleClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, params: { canClose?: boolean }) => ({
|
||||||
|
button: {
|
||||||
|
borderRadius: 4,
|
||||||
|
flex: '1 15%',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
height: 'auto',
|
||||||
|
textAlign: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 2,
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: params.canClose === false ? theme.colors.dark[2] : theme.colors.dark[0],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const HeaderButton: React.FC<Props> = ({ icon, canClose, iconSize, handleClick }) => {
|
||||||
|
const { classes } = useStyles({ canClose });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className={classes.button}
|
||||||
|
classNames={{ label: classes.label, root: classes.root }}
|
||||||
|
disabled={canClose === false}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} fontSize={iconSize} fixedWidth />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderButton;
|
||||||
@@ -1,35 +1,29 @@
|
|||||||
import { Box, Flex, useCheckbox, chakra, CheckboxIcon } from '@chakra-ui/react';
|
import { Checkbox, createStyles, Stack } from '@mantine/core';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: theme.colors.dark[7],
|
||||||
|
'&:checked': { backgroundColor: theme.colors.dark[2], borderColor: theme.colors.dark[2] },
|
||||||
|
},
|
||||||
|
inner: {
|
||||||
|
'> svg > path': {
|
||||||
|
fill: theme.colors.dark[6],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const CustomCheckbox: React.FC<{ checked: boolean }> = ({ checked }) => {
|
const CustomCheckbox: React.FC<{ checked: boolean }> = ({ checked }) => {
|
||||||
const { getCheckboxProps, getInputProps, htmlProps } = useCheckbox();
|
const { classes } = useStyles();
|
||||||
return (
|
return (
|
||||||
<chakra.label
|
<Checkbox
|
||||||
display="flex"
|
checked={checked}
|
||||||
flexDirection="row"
|
size="md"
|
||||||
alignItems="center"
|
classNames={{ root: classes.root, input: classes.input, inner: classes.inner }}
|
||||||
gridColumnGap={2}
|
/>
|
||||||
pr={3}
|
|
||||||
cursor="pointer"
|
|
||||||
{...htmlProps}
|
|
||||||
>
|
|
||||||
<input {...getInputProps()} hidden />
|
|
||||||
<Flex
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
border="2px solid"
|
|
||||||
borderColor="#909296"
|
|
||||||
rounded={'sm'}
|
|
||||||
w={5}
|
|
||||||
h={5}
|
|
||||||
{...getCheckboxProps()}
|
|
||||||
>
|
|
||||||
{checked && (
|
|
||||||
<Box w={4} h={4} bg="#909296">
|
|
||||||
<CheckboxIcon isChecked color="#25262B" />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</chakra.label>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { Box, Text } from '@chakra-ui/react';
|
import { Box, createStyles, Text } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
container: {
|
||||||
|
textAlign: 'center',
|
||||||
|
borderTopLeftRadius: theme.radius.md,
|
||||||
|
borderTopRightRadius: theme.radius.md,
|
||||||
|
backgroundColor: theme.colors.dark[6],
|
||||||
|
height: 60,
|
||||||
|
width: 384,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
fontSize: 24,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const Header: React.FC<{ title: string }> = ({ title }) => {
|
const Header: React.FC<{ title: string }> = ({ title }) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box className={classes.container}>
|
||||||
p={3}
|
<Text className={classes.heading}>{title}</Text>
|
||||||
textAlign="center"
|
|
||||||
borderTopLeftRadius="md"
|
|
||||||
borderTopRightRadius="md"
|
|
||||||
bg="#25262B"
|
|
||||||
height="60px"
|
|
||||||
width="sm"
|
|
||||||
>
|
|
||||||
<Text fontSize={24} textTransform="uppercase" fontWeight={600} fontFamily="Nunito">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Box, Flex, Stack, Text, Progress } from '@chakra-ui/react';
|
import { Box, Group, Stack, Text, Progress } from '@mantine/core';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import CustomCheckbox from './CustomCheckbox';
|
import CustomCheckbox from './CustomCheckbox';
|
||||||
import type { MenuItem } from './index';
|
import type { MenuItem } from '../../../typings';
|
||||||
|
import { createStyles } from '@mantine/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: MenuItem;
|
item: MenuItem;
|
||||||
@@ -11,35 +12,80 @@ interface Props {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, params: { iconColor?: string }) => ({
|
||||||
|
buttonContainer: {
|
||||||
|
backgroundColor: theme.colors.dark[6],
|
||||||
|
borderRadius: theme.radius.md,
|
||||||
|
padding: 2,
|
||||||
|
height: 60,
|
||||||
|
scrollMargin: 8,
|
||||||
|
'&:focus': {
|
||||||
|
backgroundColor: theme.colors.dark[4],
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttonWrapper: {
|
||||||
|
paddingLeft: 5,
|
||||||
|
paddingRight: 12,
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fontSize: 24,
|
||||||
|
color: params.iconColor || theme.colors.dark[2],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: 12,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
chevronIcon: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
},
|
||||||
|
scrollIndexValue: {
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
progressStack: {
|
||||||
|
width: '100%',
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
progressLabel: {
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex, checked }, ref) => {
|
const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex, checked }, ref) => {
|
||||||
|
const { classes } = useStyles({ iconColor: item.iconColor });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg="#25262B"
|
|
||||||
borderRadius="md"
|
|
||||||
tabIndex={index}
|
tabIndex={index}
|
||||||
scrollMargin={2}
|
className={classes.buttonContainer}
|
||||||
p={2}
|
|
||||||
height="60px"
|
|
||||||
key={`item-${index}`}
|
key={`item-${index}`}
|
||||||
_focus={{ bg: '#373A40', outline: 'none' }}
|
ref={(element: HTMLDivElement) => {
|
||||||
ref={(element) => {
|
|
||||||
if (ref)
|
if (ref)
|
||||||
// @ts-ignore i cba
|
// @ts-ignore i cba
|
||||||
return (ref.current = [...ref.current, element]);
|
return (ref.current = [...ref.current, element]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex alignItems="center" height="100%" gap="15px">
|
<Group spacing={15} noWrap className={classes.buttonWrapper}>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<Box display="flex" alignItems="center">
|
<Box className={classes.iconContainer}>
|
||||||
<FontAwesomeIcon icon={item.icon} fontSize={24} color={item.iconColor || '#909296'} fixedWidth />
|
<FontAwesomeIcon icon={item.icon} className={classes.icon} fixedWidth />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{Array.isArray(item.values) ? (
|
{Array.isArray(item.values) ? (
|
||||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
<Group position="apart" w="100%">
|
||||||
<Stack spacing={1} justifyContent="space-between">
|
<Stack spacing={0} justify="space-between">
|
||||||
<Text color="#909296" textTransform="uppercase" fontSize={12} verticalAlign="middle">
|
<Text className={classes.label}>{item.label}</Text>
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
<Text>
|
<Text>
|
||||||
{typeof item.values[scrollIndex] === 'object'
|
{typeof item.values[scrollIndex] === 'object'
|
||||||
? // @ts-ignore for some reason even checking the type TS still thinks it's a string
|
? // @ts-ignore for some reason even checking the type TS still thinks it's a string
|
||||||
@@ -47,30 +93,32 @@ const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index,
|
|||||||
: item.values[scrollIndex]}
|
: item.values[scrollIndex]}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" spacing="sm" pr={3} justifyContent="center" alignItems="center">
|
<Group spacing={1} position="center">
|
||||||
<FontAwesomeIcon icon="chevron-left" fontSize={16} color="#909296" />
|
<FontAwesomeIcon icon="chevron-left" className={classes.chevronIcon} />
|
||||||
<Text color="#909296" textTransform="uppercase" fontSize={14}>
|
<Text className={classes.scrollIndexValue}>
|
||||||
{scrollIndex + 1}/{item.values.length}
|
{scrollIndex + 1}/{item.values.length}
|
||||||
</Text>
|
</Text>
|
||||||
<FontAwesomeIcon icon="chevron-right" fontSize={16} color="#909296" />
|
<FontAwesomeIcon icon="chevron-right" className={classes.chevronIcon} />
|
||||||
</Stack>
|
</Group>
|
||||||
</Flex>
|
</Group>
|
||||||
) : item.checked !== undefined ? (
|
) : item.checked !== undefined ? (
|
||||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
<Group position="apart" w="100%">
|
||||||
<Text>{item.label}</Text>
|
<Text>{item.label}</Text>
|
||||||
<CustomCheckbox checked={checked}></CustomCheckbox>
|
<CustomCheckbox checked={checked}></CustomCheckbox>
|
||||||
</Flex>
|
</Group>
|
||||||
) : item.progress !== undefined ? (
|
) : item.progress !== undefined ? (
|
||||||
<Flex flexDirection="column" w="100%" marginRight="5px">
|
<Stack className={classes.progressStack} spacing={0}>
|
||||||
<Text verticalAlign="middle" marginBottom="3px">
|
<Text className={classes.progressLabel}>{item.label}</Text>
|
||||||
{item.label}
|
<Progress
|
||||||
</Text>
|
value={item.progress}
|
||||||
<Progress value={item.progress} size="sm" colorScheme={item.colorScheme || 'gray'} borderRadius="md" />
|
color={item.colorScheme || 'dark.0'}
|
||||||
</Flex>
|
styles={(theme) => ({ root: { backgroundColor: theme.colors.dark[3] } })}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Text>{item.label}</Text>
|
<Text>{item.label}</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +1,55 @@
|
|||||||
import { Box, Stack, Tooltip } from '@chakra-ui/react';
|
import { Box, createStyles, Stack, Tooltip } from '@mantine/core';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
import { fetchNui } from '../../../utils/fetchNui';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type { MenuPosition, MenuSettings } from '../../../typings';
|
||||||
|
|
||||||
export interface MenuItem {
|
const useStyles = createStyles((theme, params: { position?: MenuPosition; itemCount: number; selected: number }) => ({
|
||||||
label: string;
|
tooltip: {
|
||||||
progress?: number;
|
backgroundColor: theme.colors.dark[6],
|
||||||
colorScheme?: string;
|
color: theme.colors.dark[2],
|
||||||
checked?: boolean;
|
borderRadius: theme.radius.sm,
|
||||||
values?: Array<string | { label: string; description: string }>;
|
maxWidth: 350,
|
||||||
description?: string;
|
whiteSpace: 'normal',
|
||||||
icon?: IconProp;
|
},
|
||||||
iconColor?: string;
|
container: {
|
||||||
defaultIndex?: number;
|
position: 'absolute',
|
||||||
close?: boolean;
|
pointerEvents: 'none',
|
||||||
}
|
marginTop: params.position === 'top-left' || params.position === 'top-right' ? 5 : 0,
|
||||||
|
marginLeft: params.position === 'top-left' || params.position === 'bottom-left' ? 5 : 0,
|
||||||
export interface MenuSettings {
|
marginRight: params.position === 'top-right' || params.position === 'bottom-right' ? 5 : 0,
|
||||||
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
marginBottom: params.position === 'bottom-left' || params.position === 'bottom-right' ? 5 : 0,
|
||||||
title: string;
|
right: params.position === 'top-right' || params.position === 'bottom-right' ? 1 : undefined,
|
||||||
canClose?: boolean;
|
left: params.position === 'bottom-left' ? 1 : undefined,
|
||||||
items: Array<MenuItem>;
|
bottom: params.position === 'bottom-left' || params.position === 'bottom-right' ? 1 : undefined,
|
||||||
startItemIndex?: number;
|
fontFamily: 'Roboto',
|
||||||
}
|
},
|
||||||
|
buttonsWrapper: {
|
||||||
|
height: 'fit-content',
|
||||||
|
maxHeight: 415,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: params.itemCount <= 6 || params.selected === params.itemCount - 1 ? theme.radius.md : undefined,
|
||||||
|
backgroundColor: theme.colors.dark[8],
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
},
|
||||||
|
scrollArrow: {
|
||||||
|
backgroundColor: theme.colors.dark[8],
|
||||||
|
textAlign: 'center',
|
||||||
|
borderBottomLeftRadius: theme.radius.md,
|
||||||
|
borderBottomRightRadius: theme.radius.md,
|
||||||
|
height: 25,
|
||||||
|
},
|
||||||
|
scrollArrowIcon: {
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const ListMenu: React.FC = () => {
|
const ListMenu: React.FC = () => {
|
||||||
const [menu, setMenu] = useState<MenuSettings>({
|
const [menu, setMenu] = useState<MenuSettings>({
|
||||||
@@ -42,6 +63,7 @@ const ListMenu: React.FC = () => {
|
|||||||
const [checkedStates, setCheckedStates] = useState<Record<number, boolean>>({});
|
const [checkedStates, setCheckedStates] = useState<Record<number, boolean>>({});
|
||||||
const listRefs = useRef<Array<HTMLDivElement | null>>([]);
|
const listRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||||
const firstRenderRef = useRef(false);
|
const firstRenderRef = useRef(false);
|
||||||
|
const { classes } = useStyles({ position: menu.position, itemCount: menu.items.length, selected });
|
||||||
|
|
||||||
const closeMenu = (ignoreFetch?: boolean, keyPressed?: string, forceClose?: boolean) => {
|
const closeMenu = (ignoreFetch?: boolean, keyPressed?: string, forceClose?: boolean) => {
|
||||||
if (menu.canClose === false && !forceClose) return;
|
if (menu.canClose === false && !forceClose) return;
|
||||||
@@ -184,44 +206,20 @@ const ListMenu: React.FC = () => {
|
|||||||
menu.items[selected].values[indexStates[selected]].description
|
menu.items[selected].values[indexStates[selected]].description
|
||||||
: menu.items[selected].description
|
: menu.items[selected].description
|
||||||
}
|
}
|
||||||
isOpen={
|
opened={
|
||||||
isValuesObject(menu.items[selected].values)
|
isValuesObject(menu.items[selected].values)
|
||||||
? // @ts-ignore
|
? // @ts-ignore
|
||||||
!!menu.items[selected].values[indexStates[selected]].description
|
!!menu.items[selected].values[indexStates[selected]].description
|
||||||
: !!menu.items[selected].description
|
: !!menu.items[selected].description
|
||||||
}
|
}
|
||||||
bg="#25262B"
|
transitionDuration={0}
|
||||||
color="#909296"
|
classNames={{ tooltip: classes.tooltip }}
|
||||||
placement="bottom"
|
|
||||||
borderRadius="md"
|
|
||||||
fontFamily="Nunito"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
pointerEvents="none"
|
|
||||||
mt={menu.position === 'top-left' || menu.position === 'top-right' ? 5 : 0}
|
|
||||||
ml={menu.position === 'top-left' || menu.position === 'bottom-left' ? 5 : 0}
|
|
||||||
mr={menu.position === 'top-right' || menu.position === 'bottom-right' ? 5 : 0}
|
|
||||||
mb={menu.position === 'bottom-left' || menu.position === 'bottom-right' ? 5 : 0}
|
|
||||||
right={menu.position === 'top-right' || menu.position === 'bottom-right' ? 1 : undefined}
|
|
||||||
left={menu.position === 'bottom-left' ? 1 : undefined}
|
|
||||||
bottom={menu.position === 'bottom-left' || menu.position === 'bottom-right' ? 1 : undefined}
|
|
||||||
>
|
>
|
||||||
|
<Box className={classes.container}>
|
||||||
<Header title={menu.title} />
|
<Header title={menu.title} />
|
||||||
<Box
|
<Box className={classes.buttonsWrapper} onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => moveMenu(e)}>
|
||||||
width="sm"
|
|
||||||
height="fit-content"
|
|
||||||
maxHeight={415}
|
|
||||||
overflow="hidden"
|
|
||||||
borderRadius={menu.items.length <= 6 || selected === menu.items.length - 1 ? 'md' : undefined}
|
|
||||||
bg="#141517"
|
|
||||||
fontFamily="Nunito"
|
|
||||||
borderTopLeftRadius="none"
|
|
||||||
borderTopRightRadius="none"
|
|
||||||
onKeyDown={(e) => moveMenu(e)}
|
|
||||||
>
|
|
||||||
<FocusTrap active={visible}>
|
<FocusTrap active={visible}>
|
||||||
<Stack direction="column" p={2} overflowY="scroll">
|
<Stack spacing={8} p={8} sx={{ overflowY: 'scroll' }}>
|
||||||
{menu.items.map((item, index) => (
|
{menu.items.map((item, index) => (
|
||||||
<React.Fragment key={`menu-item-${index}`}>
|
<React.Fragment key={`menu-item-${index}`}>
|
||||||
{item.label && (
|
{item.label && (
|
||||||
@@ -239,8 +237,8 @@ const ListMenu: React.FC = () => {
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</Box>
|
</Box>
|
||||||
{menu.items.length > 6 && selected !== menu.items.length - 1 && (
|
{menu.items.length > 6 && selected !== menu.items.length - 1 && (
|
||||||
<Box bg="#141517" textAlign="center" borderBottomLeftRadius="md" borderBottomRightRadius="md" height={25}>
|
<Box className={classes.scrollArrow}>
|
||||||
<FontAwesomeIcon icon="chevron-down" color="#909296" fontSize={20} />
|
<FontAwesomeIcon icon="chevron-down" className={classes.scrollArrowIcon} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
152
web/src/features/menu/radial/index.tsx
Normal file
152
web/src/features/menu/radial/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { Box, createStyles } from '@mantine/core';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
||||||
|
import { fetchNui } from '../../../utils/fetchNui';
|
||||||
|
import ScaleFade from '../../../transitions/ScaleFade';
|
||||||
|
import type { RadialMenuItem } from '../../../typings';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
wrapper: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
sector: {
|
||||||
|
fill: theme.colors.dark[6],
|
||||||
|
color: theme.colors.dark[0],
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
fill: theme.fn.primaryColor(),
|
||||||
|
cursor: 'pointer',
|
||||||
|
'> g > text, > g > svg > path': {
|
||||||
|
fill: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'> g > text': {
|
||||||
|
fill: theme.colors.dark[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgroundCircle: {
|
||||||
|
fill: theme.colors.dark[6],
|
||||||
|
},
|
||||||
|
centerCircle: {
|
||||||
|
fill: theme.fn.primaryColor(),
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
fill: theme.colors[theme.primaryColor][theme.fn.primaryShade() - 1],
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
centerIconContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
centerIcon: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const degToRad = (deg: number) => deg * (Math.PI / 180);
|
||||||
|
|
||||||
|
const RadialMenu: React.FC = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean }>({
|
||||||
|
items: [],
|
||||||
|
sub: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean } | false) => {
|
||||||
|
if (!data) return setVisible(false);
|
||||||
|
if (visible) {
|
||||||
|
setVisible(false);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
setMenu(data);
|
||||||
|
setVisible(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
useNuiEvent('refreshItems', (data: RadialMenuItem[]) => {
|
||||||
|
setMenu({ ...menu, items: data });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box className={classes.wrapper} onContextMenu={() => menu.sub && fetchNui('radialBack')}>
|
||||||
|
<ScaleFade visible={visible}>
|
||||||
|
<svg width="350px" height="350px" transform="rotate(90)">
|
||||||
|
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/}
|
||||||
|
<g transform="translate(175, 175)">
|
||||||
|
<circle r={175} className={classes.backgroundCircle} />
|
||||||
|
</g>
|
||||||
|
{menu.items.map((item, index) => {
|
||||||
|
// Always draw full circle to avoid elipse circles with 2 or less items
|
||||||
|
const pieAngle = 360 / (menu.items.length < 3 ? 3 : menu.items.length);
|
||||||
|
const angle = degToRad(pieAngle / 2 + 90);
|
||||||
|
const radius = 175 * 0.65;
|
||||||
|
const iconX = 175 + Math.sin(angle) * radius;
|
||||||
|
const iconY = 175 + Math.cos(angle) * radius;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<g
|
||||||
|
transform={`rotate(-${index * pieAngle} 175 175)`}
|
||||||
|
className={classes.sector}
|
||||||
|
onClick={() => fetchNui('radialClick', index)}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={`M175.01,175.01 l175,0 A175.01,175.01 0 0,0 ${175 + 175 * Math.cos(-degToRad(pieAngle))}, ${
|
||||||
|
175 + 175 * Math.sin(-degToRad(pieAngle))
|
||||||
|
} z`}
|
||||||
|
/>
|
||||||
|
<g transform={`rotate(${index * pieAngle - 90} ${iconX} ${iconY})`} pointerEvents="none">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
x={iconX - 12.5}
|
||||||
|
y={iconY - 17.5}
|
||||||
|
icon={item.icon}
|
||||||
|
width={25}
|
||||||
|
height={25}
|
||||||
|
fixedWidth
|
||||||
|
/>
|
||||||
|
<text x={iconX} y={iconY + 25} fill="#fff" textAnchor="middle" pointerEvents="none">
|
||||||
|
{item.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<g
|
||||||
|
transform={`translate(175, 175)`}
|
||||||
|
onClick={() => {
|
||||||
|
if (menu.sub) fetchNui('radialBack');
|
||||||
|
else {
|
||||||
|
setVisible(false);
|
||||||
|
fetchNui('radialClose');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<circle r={30} className={classes.centerCircle} />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div className={classes.centerIconContainer}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={!menu.sub ? 'xmark' : 'arrow-rotate-left'}
|
||||||
|
fixedWidth
|
||||||
|
className={classes.centerIcon}
|
||||||
|
color="#fff"
|
||||||
|
size="2x"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScaleFade>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RadialMenu;
|
||||||
@@ -1,84 +1,183 @@
|
|||||||
import { useToast, type ToastPosition, Box, HStack, Text } from '@chakra-ui/react';
|
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import { toast, Toaster } from 'react-hot-toast';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Avatar, createStyles, Group, Stack, Box, Text, keyframes } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import type { NotificationProps } from '../../typings';
|
||||||
|
|
||||||
export interface NotificationProps {
|
const useStyles = createStyles((theme) => ({
|
||||||
title?: string;
|
container: {
|
||||||
description?: string;
|
width: 300,
|
||||||
duration?: number;
|
height: 'fit-content',
|
||||||
position?: ToastPosition;
|
backgroundColor: theme.colors.dark[6],
|
||||||
variant?: string;
|
color: theme.colors.dark[0],
|
||||||
status?: 'info' | 'warning' | 'success' | 'error';
|
padding: 12,
|
||||||
id?: number;
|
borderRadius: theme.radius.sm,
|
||||||
}
|
fontFamily: 'Roboto',
|
||||||
|
boxShadow: theme.shadows.sm,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 'normal',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
lineHeight: 'normal',
|
||||||
|
},
|
||||||
|
descriptionOnly: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.colors.dark[2],
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
lineHeight: 'normal',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export interface CustomNotificationProps {
|
// I hate this
|
||||||
style?: React.CSSProperties;
|
const enterAnimationTop = keyframes({
|
||||||
description?: string;
|
from: {
|
||||||
title?: string;
|
opacity: 0,
|
||||||
duration?: number;
|
transform: 'translateY(-30px)',
|
||||||
icon?: IconProp;
|
},
|
||||||
iconColor?: string;
|
to: {
|
||||||
position?: ToastPosition;
|
opacity: 1,
|
||||||
id?: number;
|
transform: 'translateY(0px)',
|
||||||
type?: string;
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const enterAnimationBottom = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(30px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateY(0px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitAnimationTop = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateY(0px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(-100%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitAnimationRight = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(100%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitAnimationLeft = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(-100%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitAnimationBottom = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateY(0px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(100%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const Notifications: React.FC = () => {
|
const Notifications: React.FC = () => {
|
||||||
const toast = useToast();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
useNuiEvent<CustomNotificationProps>('customNotify', (data) => {
|
|
||||||
if (!data.title && !data.description) return;
|
|
||||||
if (data.id && toast.isActive(data.id)) return;
|
|
||||||
if (!data.icon) {
|
|
||||||
data.icon = data.type === 'error' ? 'circle-xmark' : data.type === 'success' ? 'circle-check' : 'circle-info';
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = data.id;
|
|
||||||
toast({
|
|
||||||
id,
|
|
||||||
duration: data.duration || 3000,
|
|
||||||
position: data.position || 'top-right',
|
|
||||||
render: () => (
|
|
||||||
<Box className={`toast-${data.type || 'inform'}`} style={data.style} p={2} borderRadius="sm" boxShadow="md">
|
|
||||||
<HStack spacing={0}>
|
|
||||||
{data.icon && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
fixedWidth
|
|
||||||
icon={data.icon}
|
|
||||||
fontSize="1.3em"
|
|
||||||
style={{ paddingRight: 8 }}
|
|
||||||
color={data.iconColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box w="100%">
|
|
||||||
{data.title && <Text as="b">{data.title}</Text>}
|
|
||||||
{data.description && <Text><ReactMarkdown>{data.description}</ReactMarkdown></Text>}
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useNuiEvent<NotificationProps>('notify', (data) => {
|
useNuiEvent<NotificationProps>('notify', (data) => {
|
||||||
if (!data.title && !data.description) return;
|
if (!data.title && !data.description) return;
|
||||||
if (data.id && toast.isActive(data.id)) return;
|
// Backwards compat with old notifications
|
||||||
const id = data.id;
|
let position = data.position;
|
||||||
toast({
|
switch (position) {
|
||||||
id,
|
case 'top':
|
||||||
title: data.title,
|
position = 'top-center';
|
||||||
description: data.description,
|
break;
|
||||||
duration: data.duration || 4000,
|
case 'bottom':
|
||||||
position: data.position || 'top-right',
|
position = 'bottom-center';
|
||||||
variant: data.variant,
|
break;
|
||||||
status: data.status,
|
}
|
||||||
});
|
if (!data.icon) {
|
||||||
|
data.icon = data.type === 'error' ? 'xmark' : data.type === 'success' ? 'check' : 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.custom(
|
||||||
|
(t) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
animation: t.visible
|
||||||
|
? `${position?.includes('bottom') ? enterAnimationBottom : enterAnimationTop} 0.2s ease-out forwards`
|
||||||
|
: `${
|
||||||
|
position?.includes('right')
|
||||||
|
? exitAnimationRight
|
||||||
|
: position?.includes('left')
|
||||||
|
? exitAnimationLeft
|
||||||
|
: position === 'top-center'
|
||||||
|
? exitAnimationTop
|
||||||
|
: position
|
||||||
|
? exitAnimationBottom
|
||||||
|
: exitAnimationRight
|
||||||
|
} 0.4s ease-in forwards`,
|
||||||
|
}}
|
||||||
|
style={data.style}
|
||||||
|
className={`${classes.container}`}
|
||||||
|
>
|
||||||
|
<Group noWrap spacing={12}>
|
||||||
|
{data.icon && (
|
||||||
|
<>
|
||||||
|
{!data.iconColor ? (
|
||||||
|
<Avatar
|
||||||
|
color={data.type === 'error' ? 'red' : data.type === 'success' ? 'teal' : 'blue'}
|
||||||
|
radius="xl"
|
||||||
|
size={32}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={data.icon} fixedWidth size="lg" />
|
||||||
|
</Avatar>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={data.icon} style={{ color: data.iconColor }} fixedWidth size="lg" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Stack spacing={0}>
|
||||||
|
{data.title && <Text className={classes.title}>{data.title}</Text>}
|
||||||
|
{data.description && (
|
||||||
|
<ReactMarkdown className={!data.title ? classes.descriptionOnly : classes.description}>
|
||||||
|
{data.description}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: data.id?.toString(),
|
||||||
|
duration: data.duration || 3000,
|
||||||
|
position: position || 'top-right',
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <></>;
|
return <Toaster />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Notifications;
|
export default Notifications;
|
||||||
|
|||||||
@@ -1,14 +1,53 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CircularProgress, CircularProgressLabel, Flex, Text } from '@chakra-ui/react';
|
import { RingProgress, Text, useMantineTheme, keyframes, Stack, createStyles } from '@mantine/core';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { fetchNui } from '../../utils/fetchNui';
|
import { fetchNui } from '../../utils/fetchNui';
|
||||||
|
import ScaleFade from '../../transitions/ScaleFade';
|
||||||
|
import type { CircleProgressbarProps } from '../../typings';
|
||||||
|
|
||||||
export interface CircleProgressbarProps {
|
// 33.5 is the r of the circle
|
||||||
label?: string;
|
const progressCircle = keyframes({
|
||||||
duration: number;
|
'0%': { strokeDasharray: `0, ${33.5 * 2 * Math.PI}` },
|
||||||
position?: 'middle' | 'bottom';
|
'100%': { strokeDasharray: `${33.5 * 2 * Math.PI}, 0` },
|
||||||
percent?: boolean;
|
});
|
||||||
}
|
|
||||||
|
const useStyles = createStyles((theme, params: { position: 'middle' | 'bottom'; duration: number }) => ({
|
||||||
|
container: {
|
||||||
|
width: '100%',
|
||||||
|
height: params.position === 'middle' ? '100%' : '20%',
|
||||||
|
bottom: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
'> svg > circle:nth-child(1)': {
|
||||||
|
stroke: theme.colors.dark[5],
|
||||||
|
},
|
||||||
|
// Scuffed way of grabbing the first section and animating it
|
||||||
|
'> svg > circle:nth-child(2)': {
|
||||||
|
transition: 'none',
|
||||||
|
animation: `${progressCircle} linear forwards`,
|
||||||
|
animationDuration: `${params.duration}ms`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontFamily: 'roboto-mono',
|
||||||
|
textShadow: theme.shadows.sm,
|
||||||
|
color: theme.colors.gray[3],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
textAlign: 'center',
|
||||||
|
textShadow: theme.shadows.sm,
|
||||||
|
color: theme.colors.gray[3],
|
||||||
|
height: 25,
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
marginTop: params.position === 'middle' ? 25 : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const CircleProgressbar: React.FC = () => {
|
const CircleProgressbar: React.FC = () => {
|
||||||
const [visible, setVisible] = React.useState(false);
|
const [visible, setVisible] = React.useState(false);
|
||||||
@@ -16,7 +55,8 @@ const CircleProgressbar: React.FC = () => {
|
|||||||
const [position, setPosition] = React.useState<'middle' | 'bottom'>('middle');
|
const [position, setPosition] = React.useState<'middle' | 'bottom'>('middle');
|
||||||
const [value, setValue] = React.useState(0);
|
const [value, setValue] = React.useState(0);
|
||||||
const [label, setLabel] = React.useState('');
|
const [label, setLabel] = React.useState('');
|
||||||
const [cancelled, setCancelled] = React.useState(false);
|
const theme = useMantineTheme();
|
||||||
|
const { classes } = useStyles({ position, duration: progressDuration });
|
||||||
|
|
||||||
const progressComplete = () => {
|
const progressComplete = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
@@ -24,7 +64,6 @@ const CircleProgressbar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const progressCancel = () => {
|
const progressCancel = () => {
|
||||||
setCancelled(true);
|
|
||||||
setValue(99); // Sets the final value to 100% kek
|
setValue(99); // Sets the final value to 100% kek
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
@@ -33,7 +72,6 @@ const CircleProgressbar: React.FC = () => {
|
|||||||
|
|
||||||
useNuiEvent<CircleProgressbarProps>('circleProgress', (data) => {
|
useNuiEvent<CircleProgressbarProps>('circleProgress', (data) => {
|
||||||
if (visible) return;
|
if (visible) return;
|
||||||
setCancelled(false);
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
setValue(0);
|
setValue(0);
|
||||||
setLabel(data.label || '');
|
setLabel(data.label || '');
|
||||||
@@ -50,50 +88,23 @@ const CircleProgressbar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<>
|
||||||
h={position === 'middle' ? '100%' : '20%'}
|
<Stack spacing={0} className={classes.container}>
|
||||||
w="100%"
|
<ScaleFade visible={visible}>
|
||||||
position="absolute"
|
<Stack spacing={0} align="center" className={classes.wrapper}>
|
||||||
bottom="0"
|
<RingProgress
|
||||||
justifyContent="center"
|
size={90}
|
||||||
alignItems="center"
|
thickness={7}
|
||||||
>
|
sections={[{ value: 0, color: theme.primaryColor }]}
|
||||||
{visible && (
|
|
||||||
<Flex alignItems="center" flexDirection="column">
|
|
||||||
<CircularProgress
|
|
||||||
value={value}
|
|
||||||
size="5rem"
|
|
||||||
trackColor="rgba(0, 0, 0, 0.6)"
|
|
||||||
onAnimationEnd={progressComplete}
|
onAnimationEnd={progressComplete}
|
||||||
thickness={6}
|
className={classes.progress}
|
||||||
color={cancelled ? 'rgb(198, 40, 40)' : 'white'}
|
label={<Text className={classes.value}>{value}%</Text>}
|
||||||
sx={
|
/>
|
||||||
!cancelled
|
{label && <Text className={classes.label}>{label}</Text>}
|
||||||
? {
|
</Stack>
|
||||||
'.chakra-progress__indicator': {
|
</ScaleFade>
|
||||||
transition: 'none !important',
|
</Stack>
|
||||||
animation: 'progress linear forwards !important',
|
</>
|
||||||
animationDuration: `${progressDuration}ms !important`,
|
|
||||||
opacity: '1 !important',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
// Currently unused
|
|
||||||
'.chakra-progress__indicator': {
|
|
||||||
transition: 'none !important',
|
|
||||||
strokeDasharray: '264, 0 !important', // sets circle to full
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CircularProgressLabel fontFamily="Fira Mono">{value}%</CircularProgressLabel>
|
|
||||||
</CircularProgress>
|
|
||||||
<Text fontFamily="Inter" fontSize={18} fontWeight="light">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, Flex, Box } from '@chakra-ui/react';
|
import { Box, Text, createStyles } from '@mantine/core';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { fetchNui } from '../../utils/fetchNui';
|
import { fetchNui } from '../../utils/fetchNui';
|
||||||
|
import ScaleFade from '../../transitions/ScaleFade';
|
||||||
|
import type { ProgressbarProps } from '../../typings';
|
||||||
|
|
||||||
export interface ProgressbarProps {
|
const useStyles = createStyles((theme) => ({
|
||||||
label: string;
|
container: {
|
||||||
duration: number;
|
width: 350,
|
||||||
}
|
height: 45,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
backgroundColor: theme.colors.dark[5],
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
width: '100%',
|
||||||
|
height: '20%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bottom: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: theme.colors[theme.primaryColor][theme.fn.primaryShade()],
|
||||||
|
},
|
||||||
|
labelWrapper: {
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
width: 350,
|
||||||
|
height: 45,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
maxWidth: 350,
|
||||||
|
padding: 8,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: 20,
|
||||||
|
color: theme.colors.gray[3],
|
||||||
|
textShadow: theme.shadows.sm,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const Progressbar: React.FC = () => {
|
const Progressbar: React.FC = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
const [visible, setVisible] = React.useState(false);
|
const [visible, setVisible] = React.useState(false);
|
||||||
const [label, setLabel] = React.useState('');
|
const [label, setLabel] = React.useState('');
|
||||||
const [duration, setDuration] = React.useState(0);
|
const [duration, setDuration] = React.useState(0);
|
||||||
const [cancelled, setCancelled] = React.useState(false);
|
|
||||||
|
|
||||||
const progressComplete = () => {
|
const progressComplete = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
@@ -20,69 +58,38 @@ const Progressbar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const progressCancel = () => {
|
const progressCancel = () => {
|
||||||
setCancelled(true);
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useNuiEvent('progressCancel', progressCancel);
|
useNuiEvent('progressCancel', progressCancel);
|
||||||
|
|
||||||
useNuiEvent<ProgressbarProps>('progress', (data) => {
|
useNuiEvent<ProgressbarProps>('progress', (data) => {
|
||||||
setCancelled(false);
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
setLabel(data.label);
|
setLabel(data.label);
|
||||||
setDuration(data.duration);
|
setDuration(data.duration);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h="30%" w="100%" position="absolute" bottom="0" justifyContent="center" alignItems="center">
|
<>
|
||||||
<Box width={350}>
|
<Box className={classes.wrapper}>
|
||||||
{visible && (
|
<ScaleFade visible={visible}>
|
||||||
|
<Box className={classes.container}>
|
||||||
<Box
|
<Box
|
||||||
height={45}
|
className={classes.bar}
|
||||||
bg="rgba(0, 0, 0, 0.6)"
|
|
||||||
textAlign="center"
|
|
||||||
borderRadius="sm"
|
|
||||||
boxShadow="lg"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
height={45}
|
|
||||||
onAnimationEnd={progressComplete}
|
onAnimationEnd={progressComplete}
|
||||||
sx={
|
sx={{
|
||||||
!cancelled
|
|
||||||
? {
|
|
||||||
width: '0%',
|
|
||||||
backgroundColor: 'green.400',
|
|
||||||
animation: 'progress-bar linear',
|
animation: 'progress-bar linear',
|
||||||
animationDuration: `${duration}ms`,
|
animationDuration: `${duration}ms`,
|
||||||
}
|
}}
|
||||||
: {
|
|
||||||
// Currently unused
|
|
||||||
width: '100%',
|
|
||||||
animationPlayState: 'paused',
|
|
||||||
backgroundColor: 'rgb(198, 40, 40)',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
maxWidth={350}
|
|
||||||
fontFamily="Inter"
|
|
||||||
textOverflow="ellipsis"
|
|
||||||
overflow="hidden"
|
|
||||||
whiteSpace="nowrap"
|
|
||||||
fontSize={22}
|
|
||||||
fontWeight="light"
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
transform="translate(-50%, -50%)"
|
|
||||||
>
|
>
|
||||||
{label}
|
<Box className={classes.labelWrapper}>
|
||||||
</Text>
|
<Text className={classes.label}>{label}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Box>
|
||||||
|
</ScaleFade>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
import { Box, Center } from '@chakra-ui/react';
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { debugData } from '../../utils/debugData';
|
|
||||||
import Indicator from './indicator';
|
import Indicator from './indicator';
|
||||||
import { fetchNui } from '../../utils/fetchNui';
|
import { fetchNui } from '../../utils/fetchNui';
|
||||||
|
import { Box, createStyles } from '@mantine/core';
|
||||||
interface CustomGameDifficulty {
|
import type { SkillCheckProps, GameDifficulty } from '../../typings';
|
||||||
areaSize: number;
|
|
||||||
speedMultiplier: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GameDifficulty = 'easy' | 'medium' | 'hard' | CustomGameDifficulty;
|
|
||||||
|
|
||||||
export interface SkillCheckProps {
|
|
||||||
angle: number;
|
|
||||||
difficultyOffset: number;
|
|
||||||
difficulty: GameDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const circleCircumference = 2 * 50 * Math.PI;
|
export const circleCircumference = 2 * 50 * Math.PI;
|
||||||
|
|
||||||
@@ -28,77 +15,113 @@ const difficultyOffsets = {
|
|||||||
hard: 25,
|
hard: 25,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
svg: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
fill: 'transparent',
|
||||||
|
stroke: theme.colors.dark[5],
|
||||||
|
strokeWidth: 8,
|
||||||
|
},
|
||||||
|
skillArea: {
|
||||||
|
fill: 'transparent',
|
||||||
|
stroke: theme.fn.primaryColor(),
|
||||||
|
strokeWidth: 8,
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
stroke: 'red',
|
||||||
|
strokeWidth: 16,
|
||||||
|
fill: 'transparent',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: theme.colors.dark[5],
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 5,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const SkillCheck: React.FC = () => {
|
const SkillCheck: React.FC = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const dataRef = useRef<GameDifficulty | GameDifficulty[] | null>(null);
|
const dataRef = useRef<{ difficulty: GameDifficulty | GameDifficulty[]; inputs?: string[] } | null>(null);
|
||||||
const dataIndexRef = useRef<number>(0);
|
const dataIndexRef = useRef<number>(0);
|
||||||
const [skillCheck, setSkillCheck] = useState<SkillCheckProps>({
|
const [skillCheck, setSkillCheck] = useState<SkillCheckProps>({
|
||||||
angle: 0,
|
angle: 0,
|
||||||
difficultyOffset: 50,
|
difficultyOffset: 50,
|
||||||
difficulty: 'easy',
|
difficulty: 'easy',
|
||||||
|
key: 'e',
|
||||||
});
|
});
|
||||||
|
|
||||||
useNuiEvent('startSkillCheck', (data: GameDifficulty | GameDifficulty[]) => {
|
useNuiEvent('startSkillCheck', (data: { difficulty: GameDifficulty | GameDifficulty[]; inputs?: string[] }) => {
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
dataIndexRef.current = 0;
|
dataIndexRef.current = 0;
|
||||||
const gameData = Array.isArray(data) ? data[0] : data;
|
const gameData = Array.isArray(data.difficulty) ? data.difficulty[0] : data.difficulty;
|
||||||
const offset = typeof gameData === 'object' ? gameData.areaSize : difficultyOffsets[gameData];
|
const offset = typeof gameData === 'object' ? gameData.areaSize : difficultyOffsets[gameData];
|
||||||
|
const randomKey = data.inputs ? data.inputs[Math.floor(Math.random() * data.inputs.length)] : 'e';
|
||||||
setSkillCheck({
|
setSkillCheck({
|
||||||
angle: -90 + getRandomAngle(120, 360 - offset),
|
angle: -90 + getRandomAngle(120, 360 - offset),
|
||||||
difficultyOffset: offset,
|
difficultyOffset: offset,
|
||||||
difficulty: gameData,
|
difficulty: gameData,
|
||||||
|
key: randomKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleComplete = (success: boolean) => {
|
const handleComplete = (success: boolean) => {
|
||||||
if (!success || !Array.isArray(dataRef.current)) {
|
if (!dataRef.current) return;
|
||||||
|
if (!success || !Array.isArray(dataRef.current.difficulty)) {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
return fetchNui('skillCheckOver', success);
|
return fetchNui('skillCheckOver', success);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataIndexRef.current >= dataRef.current.length - 1) {
|
if (dataIndexRef.current >= dataRef.current.difficulty.length - 1) {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
return fetchNui('skillCheckOver', success);
|
return fetchNui('skillCheckOver', success);
|
||||||
}
|
}
|
||||||
|
|
||||||
dataIndexRef.current++;
|
dataIndexRef.current++;
|
||||||
const data = dataRef.current[dataIndexRef.current];
|
const data = dataRef.current.difficulty[dataIndexRef.current];
|
||||||
|
const key = dataRef.current.inputs
|
||||||
|
? dataRef.current.inputs[Math.floor(Math.random() * dataRef.current.inputs.length)]
|
||||||
|
: 'e';
|
||||||
const offset = typeof data === 'object' ? data.areaSize : difficultyOffsets[data];
|
const offset = typeof data === 'object' ? data.areaSize : difficultyOffsets[data];
|
||||||
setSkillCheck({
|
setSkillCheck({
|
||||||
angle: -90 + getRandomAngle(120, 360 - offset),
|
angle: -90 + getRandomAngle(120, 360 - offset),
|
||||||
difficultyOffset: offset,
|
difficultyOffset: offset,
|
||||||
difficulty: data,
|
difficulty: data,
|
||||||
|
key,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center height="100%" width="100%">
|
<>
|
||||||
{visible && (
|
{visible && (
|
||||||
<>
|
<>
|
||||||
<svg width={500} height={500}>
|
<svg r={50} width={500} height={500} className={classes.svg}>
|
||||||
{/*Circle track*/}
|
{/*Circle track*/}
|
||||||
<circle
|
<circle r={50} cx={250} cy={250} className={classes.track} strokeDasharray={circleCircumference} />
|
||||||
r={50}
|
|
||||||
cx={250}
|
|
||||||
cy={250}
|
|
||||||
fill="transparent"
|
|
||||||
stroke="rgba(0, 0, 0, 0.4)"
|
|
||||||
strokeWidth={5}
|
|
||||||
strokeDasharray={circleCircumference}
|
|
||||||
/>
|
|
||||||
{/*SkillCheck area*/}
|
{/*SkillCheck area*/}
|
||||||
<circle
|
<circle
|
||||||
r={50}
|
r={50}
|
||||||
cx={250}
|
cx={250}
|
||||||
cy={250}
|
cy={250}
|
||||||
fill="transparent"
|
|
||||||
stroke="white"
|
|
||||||
strokeDasharray={circleCircumference}
|
strokeDasharray={circleCircumference}
|
||||||
strokeDashoffset={circleCircumference - (Math.PI * 50 * skillCheck.difficultyOffset) / 180}
|
strokeDashoffset={circleCircumference - (Math.PI * 50 * skillCheck.difficultyOffset) / 180}
|
||||||
strokeWidth={5}
|
|
||||||
transform={`rotate(${skillCheck.angle}, 250, 250)`}
|
transform={`rotate(${skillCheck.angle}, 250, 250)`}
|
||||||
|
className={classes.skillArea}
|
||||||
/>
|
/>
|
||||||
<Indicator
|
<Indicator
|
||||||
angle={skillCheck.angle}
|
angle={skillCheck.angle}
|
||||||
@@ -113,27 +136,14 @@ const SkillCheck: React.FC = () => {
|
|||||||
: skillCheck.difficulty.speedMultiplier
|
: skillCheck.difficulty.speedMultiplier
|
||||||
}
|
}
|
||||||
handleComplete={handleComplete}
|
handleComplete={handleComplete}
|
||||||
|
className={classes.indicator}
|
||||||
skillCheck={skillCheck}
|
skillCheck={skillCheck}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<Box
|
<Box className={classes.button}>{skillCheck.key.toUpperCase()}</Box>
|
||||||
position="absolute"
|
|
||||||
left="50%"
|
|
||||||
top="50%"
|
|
||||||
transform="translate(-50%, -50%)"
|
|
||||||
backgroundColor="rgba(0, 0, 0, 0.4)"
|
|
||||||
w={25}
|
|
||||||
h={25}
|
|
||||||
textAlign="center"
|
|
||||||
borderRadius={5}
|
|
||||||
fontFamily="Inter"
|
|
||||||
fontSize={16}
|
|
||||||
>
|
|
||||||
E
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Center>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useKeyPress } from '../../hooks/useKeyPress';
|
import type { SkillCheckProps } from '../../typings';
|
||||||
import { SkillCheckProps } from './index';
|
import { useInterval } from '@mantine/hooks';
|
||||||
import { useInterval } from '@chakra-ui/react';
|
|
||||||
import { circleCircumference } from './index';
|
import { circleCircumference } from './index';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,55 +8,63 @@ interface Props {
|
|||||||
offset: number;
|
offset: number;
|
||||||
multiplier: number;
|
multiplier: number;
|
||||||
skillCheck: SkillCheckProps;
|
skillCheck: SkillCheckProps;
|
||||||
|
className: string;
|
||||||
handleComplete: (success: boolean) => void;
|
handleComplete: (success: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Indicator: React.FC<Props> = ({ angle, offset, multiplier, handleComplete, skillCheck }) => {
|
const Indicator: React.FC<Props> = ({ angle, offset, multiplier, handleComplete, skillCheck, className }) => {
|
||||||
const [indicatorAngle, setIndicatorAngle] = useState(-90);
|
const [indicatorAngle, setIndicatorAngle] = useState(-90);
|
||||||
const [gameState, setGameState] = useState(false);
|
const [keyPressed, setKeyPressed] = useState(false);
|
||||||
const isKeyPressed = useKeyPress('e');
|
const interval = useInterval(
|
||||||
|
() =>
|
||||||
useInterval(
|
|
||||||
() => {
|
|
||||||
setIndicatorAngle((prevState) => {
|
setIndicatorAngle((prevState) => {
|
||||||
return (prevState += multiplier);
|
return (prevState += multiplier);
|
||||||
});
|
}),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyHandler = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.key.toLowerCase() !== skillCheck.key.toLowerCase()) return;
|
||||||
|
setKeyPressed(true);
|
||||||
},
|
},
|
||||||
gameState ? 1 : null
|
[skillCheck]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIndicatorAngle(-90);
|
setIndicatorAngle(-90);
|
||||||
setGameState(true);
|
window.addEventListener('keydown', keyHandler);
|
||||||
|
interval.start();
|
||||||
}, [skillCheck]);
|
}, [skillCheck]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (indicatorAngle + 90 >= 360) {
|
if (indicatorAngle + 90 >= 360) {
|
||||||
setGameState(false);
|
interval.stop();
|
||||||
handleComplete(false);
|
handleComplete(false);
|
||||||
}
|
}
|
||||||
}, [indicatorAngle]);
|
}, [indicatorAngle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isKeyPressed) return;
|
if (!keyPressed) return;
|
||||||
|
|
||||||
setGameState(false);
|
interval.stop();
|
||||||
|
|
||||||
|
setKeyPressed(false);
|
||||||
|
window.removeEventListener('keydown', keyHandler);
|
||||||
|
|
||||||
if (indicatorAngle < angle || indicatorAngle > angle + offset) handleComplete(false);
|
if (indicatorAngle < angle || indicatorAngle > angle + offset) handleComplete(false);
|
||||||
else handleComplete(true);
|
else handleComplete(true);
|
||||||
}, [isKeyPressed]);
|
}, [keyPressed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<circle
|
<circle
|
||||||
r={50}
|
r={50}
|
||||||
cx={250}
|
cx={250}
|
||||||
cy={250}
|
cy={250}
|
||||||
fill="transparent"
|
|
||||||
stroke="red"
|
|
||||||
strokeWidth={15}
|
|
||||||
strokeDasharray={circleCircumference}
|
strokeDasharray={circleCircumference}
|
||||||
strokeDashoffset={circleCircumference - 3}
|
strokeDashoffset={circleCircumference - 3}
|
||||||
transform={`rotate(${indicatorAngle}, 250, 250)`}
|
transform={`rotate(${indicatorAngle}, 250, 250)`}
|
||||||
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../hooks/useNuiEvent';
|
||||||
import { Box, Flex, ScaleFade } from '@chakra-ui/react';
|
import { Box, createStyles, Group } from '@mantine/core';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import ScaleFade from '../../transitions/ScaleFade';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import type { TextUiProps, TextUiPosition } from '../../typings';
|
||||||
|
|
||||||
export interface TextUiProps {
|
const useStyles = createStyles((theme, params: { position?: TextUiPosition }) => ({
|
||||||
text: string;
|
wrapper: {
|
||||||
position?: 'right-center' | 'left-center' | 'top-center';
|
height: '100%',
|
||||||
icon?: IconProp;
|
width: '100%',
|
||||||
iconColor?: string;
|
position: 'absolute',
|
||||||
style?: React.CSSProperties;
|
display: 'flex',
|
||||||
}
|
alignItems: params.position === 'top-center' ? 'baseline' : 'center',
|
||||||
|
justifyContent:
|
||||||
|
params.position === 'right-center' ? 'flex-end' : params.position === 'left-center' ? 'flex-start' : 'center',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
fontSize: 16,
|
||||||
|
padding: 12,
|
||||||
|
margin: 8,
|
||||||
|
backgroundColor: theme.colors.dark[6],
|
||||||
|
color: theme.colors.dark[0],
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
boxShadow: theme.shadows.sm,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const TextUI: React.FC = () => {
|
const TextUI: React.FC = () => {
|
||||||
const [data, setData] = React.useState<TextUiProps>({
|
const [data, setData] = React.useState<TextUiProps>({
|
||||||
@@ -19,6 +35,7 @@ const TextUI: React.FC = () => {
|
|||||||
position: 'right-center',
|
position: 'right-center',
|
||||||
});
|
});
|
||||||
const [visible, setVisible] = React.useState(false);
|
const [visible, setVisible] = React.useState(false);
|
||||||
|
const { classes } = useStyles({ position: data.position });
|
||||||
|
|
||||||
useNuiEvent<TextUiProps>('textUi', (data) => {
|
useNuiEvent<TextUiProps>('textUi', (data) => {
|
||||||
if (!data.position) data.position = 'right-center'; // Default right position
|
if (!data.position) data.position = 'right-center'; // Default right position
|
||||||
@@ -29,42 +46,18 @@ const TextUI: React.FC = () => {
|
|||||||
useNuiEvent('textUiHide', () => setVisible(false));
|
useNuiEvent('textUiHide', () => setVisible(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<>
|
||||||
w="100%"
|
<Box className={classes.wrapper}>
|
||||||
h="100%"
|
<ScaleFade visible={visible}>
|
||||||
p={3}
|
<Box style={data.style} className={classes.container}>
|
||||||
position="absolute"
|
<Group spacing={12}>
|
||||||
alignItems={data.position === 'top-center' ? 'baseline' : 'center'}
|
{data.icon && <FontAwesomeIcon icon={data.icon} fixedWidth size="lg" style={{ color: data.iconColor }} />}
|
||||||
justifyContent={
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{data.text}</ReactMarkdown>
|
||||||
data.position === 'right-center' ? 'flex-end' : data.position === 'left-center' ? 'flex-start' : 'center'
|
</Group>
|
||||||
}
|
|
||||||
>
|
|
||||||
<ScaleFade in={visible} unmountOnExit>
|
|
||||||
<Box
|
|
||||||
bg="gray.700"
|
|
||||||
boxShadow="md"
|
|
||||||
p={3}
|
|
||||||
fontFamily="Poppins"
|
|
||||||
fontSize="0.95em"
|
|
||||||
style={data.style}
|
|
||||||
borderRadius="sm"
|
|
||||||
maxW="xs"
|
|
||||||
>
|
|
||||||
<Flex justifyContent="center" alignItems="center">
|
|
||||||
{data.icon && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
fixedWidth
|
|
||||||
icon={data.icon}
|
|
||||||
color={data.iconColor}
|
|
||||||
fontSize="1.3em"
|
|
||||||
style={{ paddingRight: 8 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ReactMarkdown>{data.text}</ReactMarkdown>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
</ScaleFade>
|
</ScaleFade>
|
||||||
</Flex>
|
</Box>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Mono&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Fira+Mono&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import url("https://use.typekit.net/wxh5ury.css");
|
||||||
|
@import url("https://use.typekit.net/qgr5ebd.css");
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color-scheme: normal !important;
|
color-scheme: normal !important;
|
||||||
@@ -20,6 +22,10 @@ body {
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -28,15 +34,6 @@ code {
|
|||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progress {
|
|
||||||
0% {
|
|
||||||
stroke-dasharray: 0, 264;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dasharray: 264, 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-bar {
|
@keyframes progress-bar {
|
||||||
from {
|
from {
|
||||||
width: 0%;
|
width: 0%;
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
|
||||||
|
|
||||||
export interface IInput {
|
|
||||||
type: 'input';
|
|
||||||
label: string;
|
|
||||||
placeholder?: string;
|
|
||||||
default?: string;
|
|
||||||
password?: boolean;
|
|
||||||
icon?: IconProp;
|
|
||||||
disabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICheckbox {
|
|
||||||
type: 'checkbox';
|
|
||||||
label: string;
|
|
||||||
checked?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISelect {
|
|
||||||
type: 'select';
|
|
||||||
label: string;
|
|
||||||
default?: string;
|
|
||||||
options?: { value: string; label?: string }[];
|
|
||||||
disabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INumber {
|
|
||||||
type: 'number';
|
|
||||||
label: string;
|
|
||||||
placeholder?: string;
|
|
||||||
default?: number;
|
|
||||||
icon?: IconProp;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISlider {
|
|
||||||
type: 'slider';
|
|
||||||
label: string;
|
|
||||||
default?: number;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,13 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { VisibilityProvider } from './providers/VisibilityProvider';
|
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
|
||||||
import { theme } from './theme';
|
|
||||||
import { debugData } from './utils/debugData';
|
|
||||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { far } from '@fortawesome/free-regular-svg-icons';
|
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { fab } from '@fortawesome/free-brands-svg-icons';
|
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { isEnvBrowser } from './utils/misc';
|
import { isEnvBrowser } from './utils/misc';
|
||||||
import LocaleProvider from './providers/LocaleProvider';
|
import LocaleProvider from './providers/LocaleProvider';
|
||||||
|
import ConfigProvider from './providers/ConfigProvider';
|
||||||
|
|
||||||
library.add(fas, far, fab);
|
library.add(fas, far, fab);
|
||||||
|
|
||||||
@@ -25,22 +22,13 @@ if (isEnvBrowser()) {
|
|||||||
root!.style.backgroundPosition = 'center';
|
root!.style.backgroundPosition = 'center';
|
||||||
}
|
}
|
||||||
|
|
||||||
debugData([
|
|
||||||
{
|
|
||||||
action: 'setVisible',
|
|
||||||
data: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
ReactDOM.createRoot(root!).render(
|
ReactDOM.createRoot(root!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<LocaleProvider>
|
<LocaleProvider>
|
||||||
<VisibilityProvider>
|
<ConfigProvider>
|
||||||
<ChakraProvider theme={theme}>
|
|
||||||
<App />
|
<App />
|
||||||
</ChakraProvider>
|
</ConfigProvider>
|
||||||
</VisibilityProvider>
|
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
32
web/src/providers/ConfigProvider.tsx
Normal file
32
web/src/providers/ConfigProvider.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Context, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { MantineColor } from '@mantine/core';
|
||||||
|
import { fetchNui } from '../utils/fetchNui';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
primaryColor: MantineColor;
|
||||||
|
primaryShade: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigCtxValue {
|
||||||
|
config: Config;
|
||||||
|
setConfig: (config: Config) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigCtx = createContext<{ config: Config; setConfig: (config: Config) => void } | null>(null);
|
||||||
|
|
||||||
|
const ConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [config, setConfig] = useState<Config>({
|
||||||
|
primaryColor: 'blue',
|
||||||
|
primaryShade: 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNui<Config>('getConfig').then((data) => setConfig(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <ConfigCtx.Provider value={{ config, setConfig }}>{children}</ConfigCtx.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigProvider;
|
||||||
|
|
||||||
|
export const useConfig = () => useContext<ConfigCtxValue>(ConfigCtx as Context<ConfigCtxValue>);
|
||||||
@@ -15,6 +15,7 @@ debugData([
|
|||||||
data: {
|
data: {
|
||||||
language: 'English',
|
language: 'English',
|
||||||
ui: {
|
ui: {
|
||||||
|
cancel: 'Cancel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import React, { Context, createContext, useContext, useState } from 'react';
|
|
||||||
import { useNuiEvent } from '../hooks/useNuiEvent';
|
|
||||||
|
|
||||||
const VisibilityCtx = createContext<VisibilityProviderValue | null>(null);
|
|
||||||
|
|
||||||
interface VisibilityProviderValue {
|
|
||||||
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 }) => {
|
|
||||||
const [visible, setVisible] = useState(true);
|
|
||||||
|
|
||||||
useNuiEvent<boolean>('setVisible', setVisible);
|
|
||||||
|
|
||||||
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>);
|
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
import { MantineThemeOverride } from '@mantine/core';
|
||||||
|
|
||||||
const config: ThemeConfig = {
|
export const theme: MantineThemeOverride = {
|
||||||
initialColorMode: 'dark',
|
colorScheme: 'dark',
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
shadows: { sm: '1px 1px 3px rgba(0, 0, 0, 0.5)' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const theme = extendTheme({
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|||||||
21
web/src/transitions/ScaleFade.tsx
Normal file
21
web/src/transitions/ScaleFade.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const ScaleFade: React.FC<{ visible: boolean; children: React.ReactNode }> = ({ visible, children }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, transition: { duration: 0.2, ease: [0, 0, 0.2, 1] } }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.1, ease: [0.4, 0, 1, 1] } }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScaleFade;
|
||||||
12
web/src/typings/alert.ts
Normal file
12
web/src/typings/alert.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface AlertProps {
|
||||||
|
header: string;
|
||||||
|
content: string;
|
||||||
|
centered?: boolean;
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
overflow?: boolean;
|
||||||
|
cancel?: boolean;
|
||||||
|
labels?: {
|
||||||
|
cancel?: string;
|
||||||
|
confirm?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export interface Option {
|
|||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
metadata?: string[] | { [key: string]: any } | { label: string; value: any }[];
|
metadata?: string[] | { [key: string]: any } | { label: string; value: any; progress?: number }[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
event?: string;
|
event?: string;
|
||||||
serverEvent?: string;
|
serverEvent?: string;
|
||||||
72
web/src/typings/dialog.ts
Normal file
72
web/src/typings/dialog.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
export interface InputProps {
|
||||||
|
heading: string;
|
||||||
|
rows: Array<IInput | ICheckbox | ISelect | INumber | ISlider | IColorInput | IDateInput | ITextarea | ITimeInput>;
|
||||||
|
options?: {
|
||||||
|
allowCancel?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseField<T, U> = {
|
||||||
|
type: T;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
default?: U;
|
||||||
|
icon?: IconProp;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IInput extends BaseField<'input', string> {
|
||||||
|
password?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICheckbox {
|
||||||
|
type: 'checkbox';
|
||||||
|
label: string;
|
||||||
|
checked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionValue = { value: string; label?: string };
|
||||||
|
export interface ISelect extends BaseField<'select' | 'multi-select', string | string[]> {
|
||||||
|
options: Array<OptionValue>;
|
||||||
|
clearable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INumber extends BaseField<'number', number> {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISlider extends Omit<BaseField<'slider', number>, 'description' | 'placeholder'> {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IColorInput extends BaseField<'color', string> {
|
||||||
|
format?: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDateInput
|
||||||
|
extends Omit<BaseField<'date' | 'date-range', string | [string, string] | true>, 'placeholder'> {
|
||||||
|
format?: string;
|
||||||
|
clearable?: boolean;
|
||||||
|
min?: string;
|
||||||
|
max?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimeInput extends Omit<BaseField<'time', string>, 'placeholder'> {
|
||||||
|
format?: '12' | '24';
|
||||||
|
clearable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITextarea extends BaseField<'textarea', string> {
|
||||||
|
autosize?: boolean;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
9
web/src/typings/index.ts
Normal file
9
web/src/typings/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './alert';
|
||||||
|
export * from './context';
|
||||||
|
export * from './dialog';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './notifications';
|
||||||
|
export * from './progress';
|
||||||
|
export * from './radial';
|
||||||
|
export * from './skillcheck';
|
||||||
|
export * from './textui';
|
||||||
24
web/src/typings/menu.ts
Normal file
24
web/src/typings/menu.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
export type MenuPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
label: string;
|
||||||
|
progress?: number;
|
||||||
|
colorScheme?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
values?: Array<string | { label: string; description: string }>;
|
||||||
|
description?: string;
|
||||||
|
icon?: IconProp;
|
||||||
|
iconColor?: string;
|
||||||
|
defaultIndex?: number;
|
||||||
|
close?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuSettings {
|
||||||
|
position?: MenuPosition;
|
||||||
|
title: string;
|
||||||
|
canClose?: boolean;
|
||||||
|
items: Array<MenuItem>;
|
||||||
|
startItemIndex?: number;
|
||||||
|
}
|
||||||
15
web/src/typings/notifications.ts
Normal file
15
web/src/typings/notifications.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ToastPosition } from 'react-hot-toast';
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
export interface NotificationProps {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
description?: string;
|
||||||
|
title?: string;
|
||||||
|
duration?: number;
|
||||||
|
icon?: IconProp;
|
||||||
|
iconColor?: string;
|
||||||
|
position?: ToastPosition | 'top' | 'bottom';
|
||||||
|
id?: number | string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
11
web/src/typings/progress.ts
Normal file
11
web/src/typings/progress.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface CircleProgressbarProps {
|
||||||
|
label?: string;
|
||||||
|
duration: number;
|
||||||
|
position?: 'middle' | 'bottom';
|
||||||
|
percent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressbarProps {
|
||||||
|
label: string;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
6
web/src/typings/radial.ts
Normal file
6
web/src/typings/radial.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
|
export interface RadialMenuItem {
|
||||||
|
icon: IconProp;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
13
web/src/typings/skillcheck.ts
Normal file
13
web/src/typings/skillcheck.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
interface CustomGameDifficulty {
|
||||||
|
areaSize: number;
|
||||||
|
speedMultiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameDifficulty = 'easy' | 'medium' | 'hard' | CustomGameDifficulty;
|
||||||
|
|
||||||
|
export interface SkillCheckProps {
|
||||||
|
angle: number;
|
||||||
|
difficultyOffset: number;
|
||||||
|
difficulty: GameDifficulty;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
12
web/src/typings/textui.ts
Normal file
12
web/src/typings/textui.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type TextUiPosition = 'right-center' | 'left-center' | 'top-center';
|
||||||
|
|
||||||
|
export interface TextUiProps {
|
||||||
|
text: string;
|
||||||
|
position?: TextUiPosition;
|
||||||
|
icon?: IconProp;
|
||||||
|
iconColor?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: "./",
|
base: "./",
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
target: "esnext",
|
target: "esnext",
|
||||||
|
|||||||
Reference in New Issue
Block a user