1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Nordi98 2025-06-24 04:26:19 +02:00
parent c01fa3bb74
commit 2ccf477c6c
49 changed files with 6 additions and 2 deletions

View file

@ -0,0 +1,26 @@
gs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
./svelte-source/node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,84 @@
# ps-multijob
![image](https://user-images.githubusercontent.com/82112471/205506429-6e86cadc-985c-488a-9dce-78a6b5aec1bb.png)
A script designed with a sleek and modern design for being able to display your current jobs as well as switching between them.
## Features
* Configurable ignore certain jobs.
* Configurable keybind to open the job menu - J by default.
* Configurable max jobs per citizen ID. Unlimited jobs for players with the 'admin' permission.
* Configurable white list jobs.
* Configurable descriptions per job.
* Configurable side (left or right) of the screen you want the ui to show on. Right side by default. (see Config)
* Configurable job icon via font awesome icons. Change these icons in the config
* Remove someone's job by doing /removejob - Admin only.
* Coming later: Admin Tab for job handling.
## Preview
![image](https://user-images.githubusercontent.com/82112471/206809426-155ad6fd-50d0-4ff9-add0-d72ae00f2304.png)
## Installation
* Rename to ps-multijob. Do not change the name or it will not work.
* Import [SQL](https://github.com/Project-Sloth/ps-multijob/blob/main/database.sql) into your database
* Ensure to server.cfg
### Linking to qb-management | Auto Firing
1. Find the following event
```txt
qb-bossmenu:server:FireEmployee
```
2. Insert the TriggerEvent right under the notification for 'Employee Fired!'. The TriggerEvent should be added twice, once near line 174 and once near line 199.
```lua
TriggerClientEvent('QBCore:Notify', src, "Employee fired!", "success")
TriggerEvent('ps-multijob:server:removeJob', target)
```
## Usage
### Serversided Exports
* GetJobs(citizenid)
Example usage:
```lua
local jobs = exports["ps-multijob"]:GetJobs("citizenid here")
```
* AddJob(citizenid, job, grade)
Example usage:
```lua
exports["ps-multijob"]:AddJob("citizenid here", "police", 0)
```
* UpdateJobRank(citizenid, job, grade)
Example usage:
```lua
exports["ps-multijob"]:UpdateJobRank("citizenid here", "police", 3)
```
* RemoveJob(citizenid, job)
Example usage:
```lua
exports["ps-multijob"]:RemoveJob("citizenid here", "police")
```
## Credits
* [xFutte](https://github.com/xFutte)
* [Silent](https://github.com/S1lentcodes)
* [Jay](https://github.com/jay-fivem)
* [Snipe](https://github.com/pushkart2)

View file

@ -0,0 +1,74 @@
local QBCore = exports['qb-core']:GetCoreObject()
local function GetJobs()
local p = promise.new()
QBCore.Functions.TriggerCallback('ps-multijob:getJobs', function(result)
p:resolve(result)
end)
return Citizen.Await(p)
end
local function OpenUI()
local job = QBCore.Functions.GetPlayerData().job
SetNuiFocus(true,true)
SendNUIMessage({
action = 'sendjobs',
activeJob = job["name"],
onDuty = job["onduty"],
jobs = GetJobs(),
side = Config.Side,
})
end
RegisterNUICallback('selectjob', function(data, cb)
TriggerServerEvent("ps-multijob:changeJob", data["name"], data["grade"])
local onDuty = false
if data["name"] ~= "police" then onDuty = QBCore.Shared.Jobs[data["name"]].defaultDuty end
cb({onDuty = onDuty})
end)
RegisterNUICallback('closemenu', function(data, cb)
cb({})
SetNuiFocus(false,false)
end)
RegisterNUICallback('removejob', function(data, cb)
TriggerServerEvent("ps-multijob:removeJob", data["name"], data["grade"])
local jobs = GetJobs()
jobs[data["name"]] = nil
cb(jobs)
end)
RegisterNUICallback('toggleduty', function(data, cb)
cb({})
local job = QBCore.Functions.GetPlayerData().job.name
if Config.DenyDuty[job] then
TriggerEvent("QBCore:Notify", 'Not allowed to use this station for clock-in.', 'error')
return
end
TriggerServerEvent("QBCore:ToggleDuty")
end)
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo)
SendNUIMessage({
action = 'updatejob',
name = JobInfo["name"],
label = JobInfo["label"],
onDuty = JobInfo["onduty"],
gradeLabel = JobInfo["grade"].name,
grade = JobInfo["grade"].level,
salary = JobInfo["payment"],
isWhitelist = Config.WhitelistJobs[JobInfo["name"]] or false,
description = Config.Descriptions[JobInfo["name"]] or "",
icon = Config.FontAwesomeIcons[JobInfo["name"]] or "",
})
end)
RegisterCommand("jobmenu", OpenUI, false)
RegisterKeyMapping('jobmenu', "Show Job Management", "keyboard", "J")
TriggerEvent('chat:removeSuggestion', '/jobmenu')

View file

@ -0,0 +1,60 @@
Config = Config or {}
-- Side of the screen where you want the ui to be on. Can either be "left" or "right"
Config.Side = "right"
Config.MaxJobs = 3
Config.IgnoredJobs = {
["unemployed"] = true,
}
Config.DenyDuty = {
["ambulance"] = true,
["police"] = true,
}
Config.WhitelistJobs = {
["police"] = true,
["ambulance"] = true,
["mechanic"] = true,
["judge"] = true,
["lawyer"] = true,
}
Config.Descriptions = {
["police"] = "Shoot some criminals or maybe be a good cop and arrest them",
["ambulance"] = "Fix the bullet holes",
["mechanic"] = "Fix the bullet holes",
["tow"] = "Pickup the tow truck and steal some vehicles",
["taxi"] = "Pickup people around the city and drive them to their destination",
["bus"] = "Pickup multiple people around the city and drive them to their destination",
["realestate"] = "Sell houses or something",
["cardealer"] = "Sell cars or something",
["judge"] = "Decide if people are guilty",
["lawyer"] = "Help the good or the bad",
["reporter"] = "Lowkey useless",
["trucker"] = "Drive a truck",
["garbage"] = "Drive a garbage truck",
["vineyard"] = "Get them vines",
["admin"] = "Sell them glizzys",
}
-- Change the icons to any free font awesome icon, also add other jobs your server might have to the list
-- List: https://fontawesome.com/search?o=r&s=solid
Config.FontAwesomeIcons = {
["police"] = "fa-solid fa-handcuffs",
["ambulance"] = "fa-solid fa-user-doctor",
["mechanic"] = "fa-solid fa-wrench",
["tow"] = "fa-solid fa-truck-tow",
["taxi"] = "fa-solid fa-taxi",
["bus"] = "fa-solid fa-bus",
["realestate"] = "fa-solid fa-sign-hanging",
["cardealer"] = "fa-solid fa-cards",
["judge"] = "fa-solid fa-gave",
["lawyer"] = "fa-solid fa-gavel",
["reporter"] = "fa-solid fa-microphone",
["trucker"] = "fa-solid fa-truck-front",
["garbage"] = "fa-solid fa-trash-can",
["vineyard"] = "fa-solid fa-wine-bottle",
["hotdog"] = "fa-solid fa-hotdog",
}

View file

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS `multijobs` (
`citizenid` varchar(100) NOT NULL,
`jobdata` text DEFAULT NULL,
PRIMARY KEY (`citizenid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -0,0 +1,19 @@
fx_version 'cerulean'
game 'gta5'
version '1.1.4'
shared_script 'config.lua'
client_script 'client/cl_*.lua'
server_scripts{
'@oxmysql/lib/MySQL.lua',
'server/sv_*.lua',
}
ui_page 'html/index.html'
files {
'html/*',
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PS-MultiJob</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer"><script type="module" crossorigin src="./index.js"></script><link rel="stylesheet" href="./index.css"></head><body><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,280 @@
local QBCore = exports['qb-core']:GetCoreObject()
local function GetJobs(citizenid)
local p = promise.new()
MySQL.Async.fetchAll("SELECT jobdata FROM multijobs WHERE citizenid = @citizenid",{
["@citizenid"] = citizenid
}, function(jobs)
if jobs[1] and jobs ~= "[]" then
jobs = json.decode(jobs[1].jobdata)
else
local Player = QBCore.Functions.GetOfflinePlayerByCitizenId(citizenid)
local temp = {}
if not Config.IgnoredJobs[Player.PlayerData.job.name] then
temp[Player.PlayerData.job.name] = Player.PlayerData.job.grade.level
MySQL.insert('INSERT INTO multijobs (citizenid, jobdata) VALUES (:citizenid, :jobdata) ON DUPLICATE KEY UPDATE jobdata = :jobdata', {
citizenid = citizenid,
jobdata = json.encode(temp),
})
end
jobs = temp
end
p:resolve(jobs)
end)
return Citizen.Await(p)
end
exports("GetJobs", GetJobs)
local function AddJob(citizenid, job, grade)
local jobs = GetJobs(citizenid)
for ignored in pairs(Config.IgnoredJobs) do
if jobs[ignored] then
jobs[ignored] = nil
end
end
jobs[job] = grade
MySQL.insert('INSERT INTO multijobs (citizenid, jobdata) VALUES (:citizenid, :jobdata) ON DUPLICATE KEY UPDATE jobdata = :jobdata', {
citizenid = citizenid,
jobdata = json.encode(jobs),
})
end
exports("AddJob", AddJob)
local function UpdatePlayerJob(Player, job, grade)
if Player.PlayerData.source ~= nil then
Player.Functions.SetJob(job,grade)
else -- player is offline
local sharedJobData = QBCore.Shared.Jobs[job]
if sharedJobData == nil then return end
local sharedGradeData = sharedJobData.grades[grade]
if sharedGradeData == nil then return end
local isBoss = false
if sharedGradeData.isboss then isBoss = true end
MySQL.update.await("update players set job = @jobData where citizenid = @citizenid", {
jobData = json.encode({
label = sharedJobData.label,
name = job,
isboss = isBoss,
onduty = sharedJobData.defaultDuty,
payment = sharedGradeData.payment,
grade = {
name = sharedGradeData.name,
level = grade,
},
}),
citizenid = Player.PlayerData.citizenid
})
end
end
local function UpdateJobRank(citizenid, job, grade)
local Player = QBCore.Functions.GetOfflinePlayerByCitizenId(citizenid)
if Player == nil then
return
end
local jobs = GetJobs(citizenid)
if jobs[job] == nil then
return
end
jobs[job] = grade
MySQL.update.await("update multijobs set jobdata = :jobdata where citizenid = :citizenid", {
citizenid = citizenid,
jobdata = json.encode(jobs),
})
-- if the current job matches, then update
if Player.PlayerData.job.name == job then
UpdatePlayerJob(Player, job, grade)
end
end
exports("UpdateJobRank", UpdateJobRank)
local function RemoveJob(citizenid, job)
local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
if Player == nil then
Player = QBCore.Functions.GetOfflinePlayerByCitizenId(citizenid)
end
if Player == nil then return end
local jobs = GetJobs(citizenid)
jobs[job] = nil
-- Since we removed a job, put player in a new job
local foundNewJob = false
if Player.PlayerData.job.name == job then
for k,v in pairs(jobs) do
UpdatePlayerJob(Player, k,v)
foundNewJob = true
break
end
end
if not foundNewJob then
UpdatePlayerJob(Player, "unemployed", 0)
end
MySQL.insert('INSERT INTO multijobs (citizenid, jobdata) VALUES (:citizenid, :jobdata) ON DUPLICATE KEY UPDATE jobdata = :jobdata', {
citizenid = citizenid,
jobdata = json.encode(jobs),
})
end
exports("RemoveJob", RemoveJob)
QBCore.Commands.Add('removejob', 'Remove Multi Job (Admin Only)', { { name = 'id', help = 'ID of player' }, { name = 'job', help = 'Job Name' } }, false, function(source, args)
local source = source
if source ~= 0 then
if args[1] then
local Player = QBCore.Functions.GetPlayer(tonumber(args[1]))
if Player then
if args[2] then
RemoveJob(Player.PlayerData.citizenid, args[2])
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
end, 'admin')
QBCore.Commands.Add('addjob', 'Add Multi Job (Admin Only)', { { name = 'id', help = 'ID of player' }, { name = 'job', help = 'Job Name' }, { name = 'grade', help = 'Job Grade' } }, false, function(source, args)
local source = source
if source ~= 0 then
if args[1] then
local Player = QBCore.Functions.GetPlayer(tonumber(args[1]))
if Player then
if args[2]and args[3] then
AddJob(Player.PlayerData.citizenid, args[2], args[3])
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
else
TriggerClientEvent("QBCore:Notify", source, "Wrong usage!")
end
end, 'admin')
QBCore.Functions.CreateCallback("ps-multijob:getJobs", function(source, cb)
local Player = QBCore.Functions.GetPlayer(source)
local jobs = GetJobs(Player.PlayerData.citizenid)
local multijobs = {}
local whitelistedjobs = {}
local civjobs = {}
local active = {}
local getjobs = {}
local Players = QBCore.Functions.GetPlayers()
for i = 1, #Players, 1 do
local xPlayer = QBCore.Functions.GetPlayer(Players[i])
active[xPlayer.PlayerData.job.name] = 0
if active[xPlayer.PlayerData.job.name] and xPlayer.PlayerData.job.onduty then
active[xPlayer.PlayerData.job.name] = active[xPlayer.PlayerData.job.name] + 1
end
end
for job, grade in pairs(jobs) do
if QBCore.Shared.Jobs[job] == nil then
print("The job '" .. job .. "' has been removed and is not present in your QBCore jobs. Remove it from the multijob SQL or add it back to your qbcore jobs.lua.")
else
local online = active[job] or 0
getjobs = {
name = job,
grade = grade,
description = Config.Descriptions[job],
icon = Config.FontAwesomeIcons[job],
label = QBCore.Shared.Jobs[job].label,
gradeLabel = QBCore.Shared.Jobs[job].grades[tostring(grade)].name,
salary = QBCore.Shared.Jobs[job].grades[tostring(grade)].payment,
active = online,
}
if Config.WhitelistJobs[job] then
whitelistedjobs[#whitelistedjobs+1] = getjobs
else
civjobs[#civjobs+1] = getjobs
end
end
end
multijobs = {
whitelist = whitelistedjobs,
civilian = civjobs,
}
cb(multijobs)
end)
RegisterNetEvent("ps-multijob:changeJob",function(cjob, cgrade)
local source = source
local Player = QBCore.Functions.GetPlayer(source)
if cjob == "unemployed" and cgrade == 0 then
Player.Functions.SetJob(cjob, cgrade)
return
end
local jobs = GetJobs(Player.PlayerData.citizenid)
for job, grade in pairs(jobs) do
if cjob == job and cgrade == grade then
Player.Functions.SetJob(job, grade)
end
end
end)
RegisterNetEvent("ps-multijob:removeJob",function(job, grade)
local source = source
local Player = QBCore.Functions.GetPlayer(source)
RemoveJob(Player.PlayerData.citizenid, job)
end)
-- QBCORE EVENTS
RegisterNetEvent('ps-multijob:server:removeJob', function(targetCitizenId)
MySQL.Async.execute('DELETE FROM multijobs WHERE citizenid = ?', { targetCitizenId }, function(affectedRows)
if affectedRows > 0 then
print('Removed job: ' .. targetCitizenId)
else
print('Cannot remove job: ' .. targetCitizenId)
end
end)
end)
RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source, newJob)
local source = source
local Player = QBCore.Functions.GetPlayer(source)
local jobs = GetJobs(Player.PlayerData.citizenid)
local amount = 0
local setjob = newJob
for k,v in pairs(jobs) do
amount = amount + 1
end
local maxJobs = Config.MaxJobs
if QBCore.Functions.HasPermission(source, "admin") then
maxJobs = math.huge
end
if amount < maxJobs and not Config.IgnoredJobs[setjob.name] then
local foundOldJob = jobs[setjob.name]
if not foundOldJob or foundOldJob ~= setjob.grade.level then
AddJob(Player.PlayerData.citizenid, setjob.name, setjob.grade.level)
end
end
end)

View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View file

@ -0,0 +1,34 @@
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
font-family: 'Roboto', sans-serif;
overflow: hidden;
}
:root {
--color-green: #02f1b5;
--color-orange: #ff4545;
--color-darkestblue: #131121;
--color-darkerblue: #222033;
--color-darkblue: #424057;
--color-white: #ffffff;
--color-black: #000000;
--color-lightestgrey: #dadada;
--color-lightgrey: #cacaca;
--color-grey: #797979;
--font-color: rgba(var(--theme-white), 0.87);
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: rgba(60, 60, 60, 1);
}

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PS-MultiJob</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,34 @@
{
"name": "svelte-source",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "pnpm check && vite build",
"preview": "vite preview --host",
"check": "svelte-check --tsconfig ./tsconfig.json",
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@testing-library/svelte": "^3.1.3",
"@tsconfig/svelte": "^2.0.1",
"@unocss/preset-uno": "^0.44.5",
"@unocss/reset": "^0.44.5",
"html-minifier": "^4.0.0",
"jsdom": "^20.0.0",
"sass": "^1.54.9",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"unocss": "^0.44.5",
"vite": ">=3.2.7",
"vite-plugin-windicss": "^1.8.7",
"vitest": "^0.18.1"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import CategoryMenu from './components/CategoryMenu.svelte';
import NavBar from './components/NavBar.svelte';
import { EventHandler } from './utils/eventHandler';
import DebugMode from './stores/debugStore';
import PanelStore from './stores/PanelStore';
import JobStore from './stores/JobStore';
import { mockJobMenuOpen } from './utils/mockEvent';
const { panelActive, show, side } = PanelStore;
const { jobManifest } = JobStore;
EventHandler();
document.onkeyup = PanelStore.handleKeyUp;
if (DebugMode) {
mockJobMenuOpen();
}
</script>
{#if $show}
<main class={"min-h-screen flex"+($side == "right" ? " justify-end ":" ")+(DebugMode ? "bg-dark-200": "bg-transparent")}>
{#if $side == "right"}
{#if $panelActive != ""}
<div in:fly|local="{{x: 500, duration: 500}}" out:fly|local="{{x: 500, duration: 500}}">
<CategoryMenu jobArray={$jobManifest[$panelActive] || []} panelName={$panelActive}/>
</div>
{/if}
<NavBar side={$side}/>
{:else}
<NavBar side={$side}/>
{#if $panelActive != ""}
<div in:fly|local="{{x: -500, duration: 500}}" out:fly|local="{{x: -500, duration: 500}}">
<CategoryMenu jobArray={$jobManifest[$panelActive] || []} panelName={$panelActive}/>
</div>
{/if}
{/if}
</main>
{/if}
<style>
</style>

View file

@ -0,0 +1,41 @@
<script lang="ts">
import JobCard from './JobCard.svelte';
import type { Job } from '../types/types';
export let jobArray: Array<Job> = [];
export let panelName: string = "";
</script>
<main class="w-[380px] min-h-screen block pt-[20px] select-none">
<div class="text-white px-[28px] pb-4">
<p class="category">CATEGORY</p>
<p class="category-name text-white block mt-[-5px] font-medium capitalize">
{panelName} Jobs
</p>
</div>
<div class="max-h-screen overflow-y-auto px-[28px] pb-20">
{#each jobArray as job (job.name)}
<JobCard name={job.label} nuiName={job.name} nuiRank={job.grade} icon={job.icon} description={job.description}
salary={job.salary} rank={job.gradeLabel} active={job.active} category={panelName}/>
{/each}
</div>
</main>
<style lang="scss">
main {
background: var(--color-darkestblue);
color: white;
}
.category {
font-size: 10pt;
color: var(--color-lightestgrey);
}
.category-name {
font-size: 15pt;
letter-spacing: 1px;
}
</style>

View file

@ -0,0 +1,163 @@
<script lang="ts">
import JobDetail from './atoms/JobDetail.svelte';
import SalarySVG from './atoms/svgs/SalarySVG.svelte';
import RankSVG from './atoms/svgs/RankSVG.svelte';
import ActiveSVG from './atoms/svgs/ActiveSVG.svelte';
import SelectSVG from './atoms/svgs/SelectSVG.svelte';
import CrossMarkSVG from './atoms/svgs/CrossMarkSVG.svelte';
import DeleteSVG from './atoms/svgs/DeleteSVG.svelte';
import ClockSVG from './atoms/svgs/ClockSVG.svelte';
import TaxiSVG from './atoms/svgs/TaxiSVG.svelte';
import JobStore from '../stores/JobStore';
export let name: string;
export let nuiName: string;
export let icon: string = "";
export let description: string = "";
export let salary: number;
export let rank: string;
export let nuiRank: number;
export let active: number;
export let category: string;
function getDutyText(onDuty: boolean) {
return onDuty ? "On Duty" : "Off Duty";
}
function getSelectText(select: boolean) {
return select ? "Selected" : "Unselect";
}
const { activeJob, onDuty, setActiveJob, toggleDuty, unSetActiveJob, deleteJob } = JobStore;
let isActive: boolean = false;
$: isActive = $activeJob == nuiName;
$: dutyText = getDutyText($onDuty);
let onDutyHover: boolean = false;
let transitionOnDuty: boolean = false;
let transitionOffDuty: boolean = false;
function handleOnDutyMouseEnter() {
dutyText = getDutyText(!$onDuty);
onDutyHover = true;
}
function handleOnDutyMouseLeave() {
dutyText = getDutyText($onDuty);
onDutyHover = false;
transitionOnDuty = false;
transitionOffDuty = false;
}
function handleDutyChange() {
if ($onDuty) {
transitionOffDuty = true;
transitionOnDuty = false;
} else {
transitionOnDuty = true;
transitionOffDuty = false;
}
toggleDuty();
}
let selectText: string = "selected";
let selectHover: boolean = false;
function handleOnSelectMouseEnter() {
selectText = getSelectText(false);
selectHover = true;
}
function handleOnSelectMouseLeave() {
selectText = getSelectText(true);
selectHover = false;
}
function handleUnSelectJob() {
unSetActiveJob();
selectHover = false;
selectText = "selected";
}
</script>
<main class="job w-full flex flex-col gap-4 mb-[30px] b-rd-[10px] px-[22px] py-5
relative select-none bg-[var(--color-darkerblue)] border border-[var(--color-darkblue)]">
<div class="flex flex-row items-center gap-2 text-center">
<div class="w-6 text-[var(--color-green)]">
{#if icon}
<i
class="{icon} fa-lg"
/>
{:else}
<svelte:component this={TaxiSVG} />
{/if}
</div>
<p class="text-xl tracking-wide capitalize">
{name}
</p>
<div class="w-7 text-[var(--color-darkblue)] cursor-pointer ml-auto hover:text-[var(--color-orange)]"
on:click={() => deleteJob(nuiName, nuiRank, category)}>
<svelte:component this={DeleteSVG} />
</div>
</div>
<p class="text-sm text-[var(--color-lightestgrey)]">
{description}
</p>
<div class="job-details flex gap-[12px] justify-stretch">
<JobDetail icon={SalarySVG} detail="Salary" value={salary} svgSize="w-[0.8rem]"/>
<JobDetail icon={RankSVG} detail="Rank" value={rank} svgSize="w-[1.4rem]"/>
<JobDetail icon={ActiveSVG} detail="Active" value={active} svgSize="w-[1.1rem]"/>
</div>
<div class="mt-2">
{#if !isActive}
<button class="bg-[var(--color-green)] flex flex-row h-11 items-center justify-center gap-1 b-rd-[5px] py-[10px] font-medium text-black flex-1 w-full"
on:click={() => setActiveJob(nuiName, nuiName, nuiRank)}
>
<div class="w-4">
<svelte:component this={SelectSVG} />
</div>
<p class="ml-[5px] uppercase tracking-wide">select</p>
</button>
{/if}
{#if isActive}
<div class="flex flex-row justify-between gap-2">
<button class={"flex flex-1 flex-row gap-2 border-1 b-rd-[5px] justify-center items-center h-11"+
(selectHover ? "border-[var(--color-orange)] text-[var(--color-orange)]":"")}
on:click={handleUnSelectJob} on:mouseenter={handleOnSelectMouseEnter} on:mouseleave={handleOnSelectMouseLeave}>
{#if !selectHover}
<div class="w-5">
<svelte:component this={SelectSVG}/>
</div>
{/if}
<p class="uppercase tracking-wide">
{selectText}
</p>
</button>
<div class="flex-1">
<button class={`flex flex-row justify-center items-center gap-1 h-11 border-1 b-rd-[5px] py-[10px] font-medium flex-1 w-full ` +
($onDuty ?
"border-[var(--color-green)] text-[var(--color-green)] "
: "border-[var(--color-orange)] text-[var(--color-orange)] ")+
($onDuty && !transitionOnDuty ? "hover:border-[var(--color-orange)] hover:text-[var(--color-orange)]":"")+
(!$onDuty && !transitionOffDuty ? "hover:border-[var(--color-green)] hover:text-[var(--color-green)]":"")
}
on:click={handleDutyChange} on:mouseenter={handleOnDutyMouseEnter} on:mouseleave={handleOnDutyMouseLeave}
>
{#if ($onDuty && !onDutyHover) || transitionOnDuty}
<div class="w-5">
<svelte:component this={ClockSVG} />
</div>
{/if}
{#if (!$onDuty && !onDutyHover) || transitionOffDuty}
<div class="w-[0.9rem]">
<svelte:component this={CrossMarkSVG} />
</div>
{/if}
<p class="ml-[5px] uppercase tracking-wide">{dutyText}</p>
</button>
</div>
</div>
{/if}
</div>
</main>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import NavItem from './atoms/NavItem.svelte';
import PanelStore from '../stores/PanelStore';
import type { side } from '../types/types';
export let side: side;
const { panelActive, panels } = PanelStore;
</script>
<nav class="w-[80px] min-h-screen nav flex flex-col z-10">
<div class="ps-logo w-full h-[80px]"/>
{#each $panels as item}
<NavItem name={item.name} isActive={item.name == $panelActive} icon={item.icon} {side}/>
{/each}
</nav>
<style>
.nav {
background: var(--color-darkerblue);
border-left: 1px solid var(--color-darkblue);
}
</style>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let icon: any = null;
export let detail: string;
export let value: string | number;
export let svgSize: string;
</script>
<div class="flex flex-1 flex-col items-center gap-2 b-2 b-rd-2 border-[var(--color-darkblue)] pt-[14px] pb-[14px]">
<div class="w-full flex justify-center text-white">
<div class={svgSize}>
<svelte:component this={icon}/>
</div>
</div>
<div class="text-center">
<p class="text-xs">
{detail}:
<span class="text-[var(--color-green)]">
{value}
</span>
</p>
</div>
</div>

View file

@ -0,0 +1,38 @@
<script lang="ts">
import PanelStore from "../../stores/PanelStore";
import type { side } from '../../types/types';
export let icon: any;
export let isActive: boolean;
export let name: string;
export let side: side;
function navItemClicked(item: string): void {
if (isActive) {
PanelStore.setActive("");
} else {
PanelStore.setActive(item);
}
}
</script>
<div class={"navitem w-full h-[60px] flex justify-center items-center cursor-pointer duration-200 "+
(side == "left" ? "border-l-4 " : "border-r-4 ")+
(isActive ? side == "left" ? "border-l-[var(--color-green)] bg-[var(--color-darkestblue)] ": "border-r-[var(--color-green)] bg-[var(--color-darkestblue)] "
: side == "left" ? "border-l-transparent ": "border-r-transparent ")}
on:click={() => navItemClicked(name)}
>
<div class="icon">
<svelte:component this={icon} color={isActive ? "var(--color-green)" : "var(--color-grey)"}/>
</div>
</div>
<style>
.icon {
width: 40%;
color: var(--color-lightestgrey);
}
.navitem:hover {
background-color: var(--color-darkestblue);
}
</style>

View file

@ -0,0 +1,5 @@
<svg fill="currentColor" viewBox="0 0 448 512">
<path d="M224 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 224 256zM274.7 304H173.3C77.61
304 0 381.6 0 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304z"
/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View file

@ -0,0 +1,7 @@
<script lang="ts">
export let color: string = "black";
</script>
<svg fill={color} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M0 32v448h448V32H0zm316.5 325.2L224 445.9l-92.5-88.7 64.5-184-64.5-86.6h184.9L252 173.2l64.5 184z"/>
</svg>

View file

@ -0,0 +1,6 @@
<svg fill="currentColor" viewBox="0 0 512 512">
<path d="M256 512C114.6 512 0 397.4 0 256S114.6 0 256 0S512 114.6 512 256s-114.6 256-256 256zM232 120V256c0
8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24
10.7-24 24z"
/>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View file

@ -0,0 +1,5 @@
<svg fill="currentColor" viewBox="0 0 320 512">
<path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3
0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5
12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View file

@ -0,0 +1,4 @@
<svg fill="currentColor" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View file

@ -0,0 +1,14 @@
<svg fill="currentColor" viewBox="0 0 576 512">
<path d="M0 48C0 21.49 21.49 0 48 0H336C362.5 0 384 21.49 384 48V207L341.6 224H272C263.2 224 256 231.2 256
240V304C256 304.9 256.1 305.7 256.2 306.6C258.5 364.7 280.3 451.4 354.9 508.1C349.1 510.6 342.7 512 336 512H240V432C240
405.5 218.5 384 192 384C165.5 384 144 405.5 144 432V512H48C21.49 512 0 490.5 0 464V48zM80 224C71.16 224 64 231.2 64 240V272C64
280.8 71.16 288 80 288H112C120.8 288 128 280.8 128 272V240C128 231.2 120.8 224 112 224H80zM160 272C160 280.8 167.2 288 176
288H208C216.8 288 224 280.8 224 272V240C224 231.2 216.8 224 208 224H176C167.2 224 160 231.2 160 240V272zM64 144C64 152.8 71.16
160 80 160H112C120.8 160 128 152.8 128 144V112C128 103.2 120.8 96 112 96H80C71.16 96 64 103.2 64 112V144zM176 96C167.2 96 160
103.2 160 112V144C160 152.8 167.2 160 176 160H208C216.8 160 224 152.8 224 144V112C224 103.2 216.8 96 208 96H176zM256 144C256
152.8 263.2 160 272 160H304C312.8 160 320 152.8 320 144V112C320 103.2 312.8 96 304 96H272C263.2 96 256 103.2 256 112V144zM423.1
225.7C428.8 223.4 435.2 223.4 440.9 225.7L560.9 273.7C570 277.4 576 286.2 576 296C576 359.3 550.1 464.8 441.2 510.2C435.3 512.6
428.7 512.6 422.8 510.2C313.9 464.8 288 359.3 288 296C288 286.2 293.1 277.4 303.1 273.7L423.1 225.7zM432 273.8V461.7C500.2 428.7
523.5 362.7 527.4 311.1L432 273.8z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,10 @@
<svg fill="currentColor" viewBox="0 0 576 512">
<path d="M287.9 0C297.1 0 305.5 5.25 309.5 13.52L378.1 154.8L531.4 177.5C540.4 178.8 547.8 185.1 550.7 193.7C553.5 202.4
551.2 211.9 544.8 218.2L433.6 328.4L459.9 483.9C461.4 492.9 457.7 502.1 450.2 507.4C442.8 512.7 432.1 513.4 424.9 509.1L287.9
435.9L150.1 509.1C142.9 513.4 133.1 512.7 125.6 507.4C118.2 502.1 114.5 492.9 115.1 483.9L142.2 328.4L31.11 218.2C24.65 211.9
22.36 202.4 25.2 193.7C28.03 185.1 35.5 178.8 44.49 177.5L197.7 154.8L266.3 13.52C270.4 5.249 278.7 0 287.9 0L287.9 0zM287.9
78.95L235.4 187.2C231.9 194.3 225.1 199.3 217.3 200.5L98.98 217.9L184.9 303C190.4 308.5 192.9 316.4 191.6 324.1L171.4 443.7L276.6
387.5C283.7 383.7 292.2 383.7 299.2 387.5L404.4 443.7L384.2 324.1C382.9 316.4 385.5 308.5 391 303L476.9 217.9L358.6 200.5C350.7
199.3 343.9 194.3 340.5 187.2L287.9 78.95z"
/>
</svg>

After

Width:  |  Height:  |  Size: 885 B

View file

@ -0,0 +1,13 @@
<svg fill="currentColor" viewBox="0 0 320 512">
<path d="M160 0C177.7 0 192 14.33 192 32V67.68C193.6 67.89 195.1 68.12 196.7 68.35C207.3 69.93 238.9 75.02 251.9 78.31C268.1
82.65 279.4 100.1 275 117.2C270.7 134.3 253.3 144.7 236.1 140.4C226.8 137.1 198.5 133.3 187.3 131.7C155.2 126.9 127.7 129.3
108.8 136.5C90.52 143.5 82.93 153.4 80.92 164.5C78.98 175.2 80.45 181.3 82.21 185.1C84.1 189.1 87.79 193.6 95.14 198.5C111.4
209.2 136.2 216.4 168.4 225.1L171.2 225.9C199.6 233.6 234.4 243.1 260.2 260.2C274.3 269.6 287.6 282.3 295.8 299.9C304.1 317.7
305.9 337.7 302.1 358.1C295.1 397 268.1 422.4 236.4 435.6C222.8 441.2 207.8 444.8 192 446.6V480C192 497.7 177.7 512 160 512C142.3
512 128 497.7 128 480V445.1C127.6 445.1 127.1 444.1 126.7 444.9L126.5 444.9C102.2 441.1 62.07 430.6 35 418.6C18.85 411.4 11.58
392.5 18.76 376.3C25.94 360.2 44.85 352.9 60.1 360.1C81.9 369.4 116.3 378.5 136.2 381.6C168.2 386.4 194.5 383.6 212.3 376.4C229.2
369.5 236.9 359.5 239.1 347.5C241 336.8 239.6 330.7 237.8 326.9C235.9 322.9 232.2 318.4 224.9 313.5C208.6 302.8 183.8 295.6 151.6
286.9L148.8 286.1C120.4 278.4 85.58 268.9 59.76 251.8C45.65 242.4 32.43 229.7 24.22 212.1C15.89 194.3 14.08 174.3 17.95 153C25.03
114.1 53.05 89.29 85.96 76.73C98.98 71.76 113.1 68.49 128 66.73V32C128 14.33 142.3 0 160 0V0z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,7 @@
<svg fill="currentColor" x="0px" y="0px" viewBox="0 0 562.7 502.7" xml:space="preserve">
<path d="M547.3,112.1c-98,98.1-196,196.2-294,294.2c-11.9,11.9-23.7,23.8-35.6,35.6c-6,6-9.3,6-15.3,0
C141.5,381.1,80.7,320.2,19.8,259.3c-6-6-6-9.2,0-15.2c13.6-13.6,27.2-27.2,40.8-40.8c6-6,9.2-6,15.2,0
c43.2,43.2,86.5,86.4,129.7,129.7c1.3,1.3,2.4,3,3.8,4.8c2-1.9,3.4-3.1,4.7-4.4c90.5-90.5,181-181,271.5-271.5
c7.7-7.7,10-7.7,17.8,0.1c12.7,12.7,25.5,25.5,38.2,38.2c2.1,2.1,3.9,4.4,5.8,6.6C547.3,108.6,547.3,110.4,547.3,112.1z"
/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

View file

@ -0,0 +1,10 @@
<svg fill="currentColor" viewBox="0 0 576 512">
<path d="M352 0C369.7 0 384 14.33 384 32V64L384 64.15C422.6 66.31 456.3 91.49 469.2 128.3L504.4 228.8C527.6 238.4 544
261.3 544 288V480C544 497.7 529.7 512 512 512H480C462.3 512 448 497.7 448 480V432H128V480C128 497.7 113.7 512 96 512H64C46.33
512 32 497.7 32 480V288C32 261.3 48.36 238.4 71.61 228.8L106.8 128.3C119.7 91.49 153.4 66.31 192 64.15L192 64V32C192 14.33
206.3 0 224 0L352 0zM197.4 128C183.8 128 171.7 136.6 167.2 149.4L141.1 224H434.9L408.8 149.4C404.3 136.6 392.2 128 378.6
128H197.4zM128 352C145.7 352 160 337.7 160 320C160 302.3 145.7 288 128 288C110.3 288 96 302.3 96 320C96 337.7 110.3 352
128 352zM448 288C430.3 288 416 302.3 416 320C416 337.7 430.3 352 448 352C465.7 352 480 337.7 480 320C480 302.3 465.7 288
448 288z"
/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let color: string = "black";
</script>
<svg fill={color} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M152.1 38.16C161.9 47.03 162.7 62.2 153.8 72.06L81.84 152.1C77.43 156.9 71.21 159.8 64.63 159.1C58.05
160.2 51.69 157.6 47.03 152.1L7.029 112.1C-2.343 103.6-2.343 88.4 7.029 79.03C16.4 69.66 31.6 69.66 40.97 79.03L63.08
101.1L118.2 39.94C127 30.09 142.2 29.29 152.1 38.16V38.16zM152.1 198.2C161.9 207 162.7 222.2 153.8 232.1L81.84 312.1C77.43
316.9 71.21 319.8 64.63 319.1C58.05 320.2 51.69 317.6 47.03 312.1L7.029 272.1C-2.343 263.6-2.343 248.4 7.029 239C16.4
229.7 31.6 229.7 40.97 239L63.08 261.1L118.2 199.9C127 190.1 142.2 189.3 152.1 198.2V198.2zM224 96C224 78.33 238.3 64
256 64H480C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H256C238.3 128 224 113.7 224 96V96zM224 256C224 238.3
238.3 224 256 224H480C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H256C238.3 288 224 273.7 224 256zM160 416C160
398.3 174.3 384 192 384H480C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H192C174.3 448 160 433.7 160 416zM0
416C0 389.5 21.49 368 48 368C74.51 368 96 389.5 96 416C96 442.5 74.51 464 48 464C21.49 464 0 442.5 0 416z"
/>
</svg>

View file

@ -0,0 +1,9 @@
import App from './App.svelte'
import 'uno.css'
import '@unocss/reset/tailwind.css'
const app = new App({
target: document.getElementById('app')
})
export default app

View file

@ -0,0 +1,120 @@
import { writable, Writable, get } from "svelte/store";
import fetchNUI from '../utils/fetch';
import type { Job, JobManifest, side, nuiUpdateJobMessage } from '../types/types';
import PanelStore from "./PanelStore";
export interface nuiOpenMessage {
activeJob: string;
onDuty: boolean;
jobs: JobManifest;
side: side;
}
interface JobState {
jobManifest: Writable<JobManifest>;
activeJob: Writable<string>;
onDuty: Writable<boolean>;
}
const store = () => {
const JobStore: JobState = {
jobManifest: writable({
"civilian": [],
"whitelist": [],
}),
activeJob: writable("police person"),
onDuty: writable(false),
}
const methods = {
deleteJob(nuiName: string, nuiRank: number, category: string) {
fetchNUI("removejob", {
name: nuiName,
grade: nuiRank,
});
// Remove job from list
JobStore.jobManifest.update((state) => {
state[category] = state[category].filter((element: Job) => element.name != nuiName);
return state;
});
},
receiveOpenMessage(data: nuiOpenMessage) {
JobStore.jobManifest.set(data.jobs);
JobStore.activeJob.set(data.activeJob);
JobStore.onDuty.set(data.onDuty);
PanelStore.side.set(data.side || "right");
},
recieveUpdateJob(data: nuiUpdateJobMessage) {
const activeJob: string = get(JobStore.activeJob);
if (activeJob == data.name) {
JobStore.onDuty.set(data.onDuty);
}
JobStore.jobManifest.update((state) => {
function updateJob(kind: "whitelist" | "civilian", index: number) {
let changeJob = state[kind][index];
changeJob.grade = data.grade;
changeJob.gradeLabel = data.gradeLabel;
changeJob.salary = data.salary;
}
function newJob(): Job {
return {
name: data.name,
label: data.label,
description: data.description,
salary: data.salary,
gradeLabel: data.gradeLabel,
grade: data.grade,
active: 0,
icon: data.icon,
}
}
let findSameName = (job: Job) => {
return job.name == data.name
}
const accessString: "civilian" | "whitelist" = data.isWhitelist ? "whitelist" : "civilian";
let index = state[accessString]?.findIndex(findSameName);
if (index != -1) {
updateJob(accessString, index);
} else {
state[accessString] = [...state[accessString], newJob()]
}
return state;
})
},
async setActiveJob(jobName: string, nuiName: string, nuiRank: number) {
JobStore.activeJob.set(jobName);
// Needs to give back onDuty
let data = await fetchNUI("selectjob", {
name: nuiName,
grade: nuiRank,
});
JobStore.onDuty.set(data?.onDuty);
},
unSetActiveJob() {
JobStore.activeJob.set("");
JobStore.onDuty.set(false);
// Unselect current job by setting player to unemployed
fetchNUI("selectjob", {
name: 'unemployed',
grade: 0,
});
},
toggleDuty() {
JobStore.onDuty.update(state => !state);
fetchNUI('toggleduty', null);
}
}
return {
...JobStore,
...methods
}
}
export default store();

View file

@ -0,0 +1,62 @@
import { writable, Writable } from "svelte/store";
import type { side } from '../types/types';
import fetchNUI from '../utils/fetch';
import CivilianSVG from '../components/atoms/svgs/CivilianSVG.svelte';
import WhiteListSVG from '../components/atoms/svgs/WhitelistSVG.svelte';
interface panel {
name: string;
icon: any;
}
interface PanelState {
show: Writable<boolean>;
panelActive: Writable<string>;
panels: Writable<Array<panel>>;
side: Writable<side>;
}
const panels: Array<panel> = [
{
name: "whitelist",
icon: WhiteListSVG,
},
{
name: "civilian",
icon: CivilianSVG,
}
]
const store = () => {
const PanelStore: PanelState = {
panelActive: writable(""),
panels: writable(panels),
show: writable(false),
side: writable("right"),
}
const methods = {
handleKeyUp(event: KeyboardEvent) {
if (event.key == "Escape") {
methods.setShow(false);
fetchNUI("closemenu", null);
}
},
setActive(name: string) {
PanelStore.panelActive.set(name);
},
setShow(show: boolean) {
PanelStore.show.set(show);
},
setSide(side: side) {
PanelStore.side.set(side);
}
}
return {
...PanelStore,
...methods
}
}
export default store();

View file

@ -0,0 +1,2 @@
const debugMode: boolean = import.meta.env.DEV;
export default debugMode;

View file

@ -0,0 +1,7 @@
import { describe, expect, test } from 'vitest';
describe('function()', async () => {
test('behavior', () => {
expect(1).toBe(1);
});
});

View file

@ -0,0 +1,22 @@
export interface Job {
name: string;
label: string;
description: string;
salary: number;
gradeLabel: string;
grade: number;
active: number;
icon: string;
}
export interface nuiUpdateJobMessage extends Omit<Job, "active"> {
isWhitelist: boolean;
onDuty: boolean;
}
export interface JobManifest {
"whitelist": Array<Job>;
"civilian": Array<Job>;
}
export type side = "left" | "right";

View file

@ -0,0 +1,33 @@
import { onMount, onDestroy } from "svelte";
import JobStore from '../stores/JobStore';
import PanelStore from '../stores/PanelStore';
interface nuiMessage {
data: {
action: string,
[key: string]: any,
},
}
export function EventHandler() {
function mainEvent(event: nuiMessage) {
switch (event.data.action) {
case "sendjobs":
JobStore.receiveOpenMessage(event.data as any);
PanelStore.setShow(true);
break;
case "updatejob":
JobStore.recieveUpdateJob(event.data as any);
break;
}
}
onMount(() => window.addEventListener("message", mainEvent));
onDestroy(() => window.removeEventListener("message", mainEvent));
}
export function handleKeyUp(event: KeyboardEvent) {
const charCode = event.key;
if (charCode == "Escape") {
}
}

View file

@ -0,0 +1,26 @@
export default async function fetchNui(eventName: string, data: unknown = {}) {
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
const getResourceName = () => {
try {
// @ts-ignore
return window.GetParentResourceName();
} catch(err) {
return "ps-multijob";
}
}
const resourceName = getResourceName();
try {
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
return await resp.json();
} catch(err) {
}
}

View file

@ -0,0 +1,132 @@
import type { JobManifest } from '../types/types';
import type { nuiOpenMessage } from '../stores/JobStore';
export default function mockEventCall(data: unknown = {}) {
window.dispatchEvent(
new MessageEvent("message", {data})
);
};
export function exampleCall() {
setTimeout(() => {
mockEventCall({
action: 'show',
data: {
header: "Some Header!",
},
});
}, 100);
};
export function mockJobMenuOpen() {
const mockJobManifest: JobManifest = {
"whitelist": [
{
name: "police person",
label: "police person",
description: `Generate Lorem lpsum placeholder text.
Select the number of characters, words, sentences or paragraphs, and hit generate!`,
salary: 250,
gradeLabel: "Regular",
grade: 0,
active: 0,
icon: "fa-solid fa-trash-can",
},
{
name: "police chief",
label: "police chief",
description: "Blah blah blah",
salary: 500,
gradeLabel: "Boss",
grade: 0,
active: 1,
icon: "",
},
{
name: "police chief2",
label: "police chief2",
description: "Blah blah blah",
salary: 500,
gradeLabel: "Boss",
grade: 0,
active: 1,
icon: "",
},
{
name: "police chief3",
label: "police chief3",
description: "Blah blah blah",
salary: 500,
gradeLabel: "Boss",
grade: 0,
active: 1,
icon: "",
},
{
name: "police chief4",
label: "police chief4",
description: "Blah blah blah",
salary: 500,
gradeLabel: "Boss",
grade: 0,
active: 1,
icon: "",
},
],
"civilian": [
{
name: "taxi driver",
label: "taxi driver",
description: `Generate Lorem lpsum placeholder text.
Select the number of characters, words, sentences or paragraphs, and hit generate!`,
salary: 150,
gradeLabel: "Regular",
grade: 0,
active: 0,
icon: "",
},
{
name: "murdershot1",
label: "murdershot1",
description: "Take people's order and serve them food",
salary: 100,
gradeLabel: "Cashier",
grade: 0,
active: 0,
icon: "",
},
{
name: "murdershot2",
label: "murdershot2",
description: "Take people's order and serve them food",
salary: 100,
gradeLabel: "Cashier",
grade: 0,
active: 0,
icon: "",
},
{
name: "murdershot3",
label: "murdershot3",
description: "Take people's order and serve them food",
salary: 100,
gradeLabel: "Cashier",
grade: 0,
active: 0,
icon: "",
}
],
}
setTimeout(() => {
let sendData: nuiOpenMessage = {
activeJob: "murdershot1",
jobs: mockJobManifest,
onDuty: true,
side: "right",
}
mockEventCall({
action: 'sendjobs',
...sendData,
});
}, 1000);
}

View file

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

View file

@ -0,0 +1,7 @@
import sveltePreprocess from 'svelte-preprocess'
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}

View file

@ -0,0 +1,21 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

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

View file

@ -0,0 +1,50 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { minify } from "html-minifier";
import Unocss from 'unocss/vite'
import presetUno from '@unocss/preset-uno'
const minifyHtml = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return minify(html, {
collapseWhitespace: true,
});
},
};
};
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production';
return {
plugins: [
Unocss({
presets: [ presetUno() ],
}),
svelte(),
isProduction && minifyHtml(),
],
test: {
globals: true,
environment: 'jsdom',
},
base: './', // fivem nui needs to have local dir reference
build: {
minify: isProduction,
emptyOutDir: true,
outDir: '../html',
assetsDir: './',
rollupOptions: {
output: {
// By not having hashes in the name, you don't have to update the manifest, yay!
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name].[ext]`
}
}
},
};
});