This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
@font-face {
font-family: 'Roboto';
src: url("../roboto.ttf");
font-display: swap;
}
:root {
--brand: #FB9B04;
--darkest: #161616;
--dark: #252525;
--mid: #383838;
--light: #969696;
--lightest: #F2F2F2;
--toolbar-h: 50px;
--radius-lg: 16px;
--radius-md: 12px;
--radius-sm: 8px;
--shadow-1: 0 10px 30px rgba(0,0,0,.35);
--shadow-2: 0 6px 18px rgba(0,0,0,.25);
--transition-fast: 160ms cubic-bezier(.2,.7,.2,1);
--transition-med: 260ms cubic-bezier(.2,.7,.2,1);
}
html, body {
width: 100%;
height: 100%;
background: transparent;
}
body {
margin: 0;
background: transparent;
color: var(--lightest);
font: 500 14px/1.45 'Roboto', system-ui, -apple-system, Segoe UI, Arial, sans-serif;
overflow: hidden;
}
#container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(1000px, calc(100vw - 64px));
height: min(700px, calc(100vh - 64px));
display: none;
background: linear-gradient(180deg, var(--dark), var(--darkest));
border: 1px solid rgba(255,255,255,.06);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-1);
grid-template-columns: 280px 1fr;
grid-template-rows: 1fr;
overflow: hidden;
}
#container::before {
content: '';
height: 56px;
background:
linear-gradient(90deg, rgba(251,155,4,.24) 0%, transparent 40%),
linear-gradient(180deg, rgba(255,255,255,.06), transparent);
border-bottom: 1px solid rgba(255,255,255,.06);
position: absolute;
top: 0; left: 0; right: 0;
z-index: 0;
}
/*
=============
Header
=============
*/
#header {
grid-column: 1 / 2;
padding-top: 56px;
}
/* Title + close */
#header .title {
padding: 18px 20px 10px 20px;
font-size: 20px;
font-weight: 700;
letter-spacing: .2px;
color: var(--lightest);
z-index: 1;
}
#header .close {
position: absolute;
top: 10px;
right: 12px;
width: 36px;
height: 36px;
display: grid;
place-items: center;
color: var(--lightest);
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.08);
border-radius: 50%;
cursor: pointer;
font-size: 18px;
transition: transform var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
}
#header .close:hover {
transform: scale(1.06);
background: rgba(251,155,4,.15);
border-color: rgba(251,155,4,.35);
}
/* Sidebar tabs */
#header ul {
list-style: none;
margin: 8px 0 0 0;
padding: 10px 8px 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
z-index: 1;
}
#header li {
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 0px 12px 22px;
width: 100%;
box-sizing: border-box;
color: var(--lightest);
border-radius: var(--radius-md);
border: 1px solid transparent;
position: relative;
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
#header li::before {
content: '';
position: absolute;
left: 6px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 60%;
border-radius: 4px;
background: transparent;
}
#header li:hover {
background: rgba(56,56,56,.45);
border-color: rgba(255,255,255,.08);
}
#header li.selected {
background: rgba(56,56,56,.6);
border-color: rgba(251,155,4,.45);
color: var(--lightest);
}
#header li.selected::before {
background: var(--brand);
}
/* Main content area */
#menu {
grid-column: 2 / -1;
padding-top: 56px;
padding-left:8px;
height: 100%;
box-sizing: border-box;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content,
.impounded_content {
width: 100%;
flex: 1 1 auto;
min-height: 0;
color: var(--lightest);
overflow: hidden;
display: flex;
flex-direction: column;
}
.content .empty,
.impounded_content .empty {
opacity: .7;
font-weight: 500;
text-align: center;
margin-top: 18vh;
}
.content .vehicle-list,
.impounded_content .vehicle-list {
flex: 1 1 0%;
min-height: 0;
overflow-y: scroll;
scrollbar-gutter: auto;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
overflow-x: hidden;
height: calc(100% - var(--toolbar-h));
width: 96%;
max-width: 96%;
margin: 0 auto;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 12px;
padding: 10px 0 0 0;
row-gap: 36px;
align-content: start;
align-items: start;
justify-items: stretch;
grid-auto-rows: auto;
}
.content .vehicle-list::after,
.impounded_content .vehicle-list::after {
content: '';
display: block;
}
/*
=============
Search Bar
=============
*/
.toolbar {
display: flex;
align-items: center;
min-width: 0;
width: 96%;
padding:10px 0px 0px 0px;
margin: 0 auto;
box-sizing: border-box;
overflow: hidden;
flex: 0 0 auto;
height: var(--toolbar-h);
}
.toolbar input[type="search"] {
flex: 1 1 auto;
width: 100%;
height: 100%;
display: block;
padding: 10px 12px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255,255,255,.08);
background: rgba(56,56,56,.55);
color: var(--lightest);
outline: none;
}
.toolbar input[type="search"]::placeholder {
color: rgba(242,242,242,.55);
}
.toolbar input[type="search"]:focus {
border-color: rgba(251,155,4,.55);
box-shadow: 0 0 0 3px rgba(251,155,4,.12);
}
/*
=============
Vehicle List
=============
*/
@media (max-width: 820px) {
.content .vehicle-list,
.impounded_content .vehicle-list {
grid-template-columns: 1fr;
}
}
.vehicle-listing:hover {
border-color: rgba(251,155,4,.45);
box-shadow: var(--shadow-2);
z-index: 1;
}
.vehicle-listing div {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--lightest);
}
.vehicle-listing strong {
color: var(--lightest);
}
.content .vehicle-list,
.impounded_content .vehicle-list {
scrollbar-width: none;
}
.content .vehicle-list::-webkit-scrollbar,
.impounded_content .vehicle-list::-webkit-scrollbar { width: 0 !important; height: 0 !important; }
.content .vehicle-list::-webkit-scrollbar-track,
.impounded_content .vehicle-list::-webkit-scrollbar-track { background: transparent; }
.content .vehicle-list::-webkit-scrollbar-thumb,
.impounded_content .vehicle-list::-webkit-scrollbar-thumb {
background: transparent;
border: 0;
min-height: 0;
}
.content .vehicle-list::-webkit-scrollbar-thumb:hover,
.impounded_content .vehicle-list::-webkit-scrollbar-thumb:hover { filter: brightness(1.05); }
/* Vehicle card */
.vehicle-listing {
display: flex;
flex-direction: column;
gap: 10px;
background: linear-gradient(180deg, rgba(56,56,56,.45), rgba(37,37,37,.85));
border: 1px solid rgba(255,255,255,.06);
border-radius: var(--radius-md);
padding: 16px;
min-height: 148px;
width: 100%;
box-sizing: border-box;
position: relative;
z-index: 0;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
/* Condition bar */
.condition {
display: flex;
align-items: center;
gap: 8px;
}
.condition .bar {
flex: 1;
height: 8px;
background: rgba(255,255,255,.08);
border-radius: 999px;
overflow: hidden;
}
.condition .fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #ffad2e, var(--brand));
border-radius: 999px;
transition: width var(--transition-med);
}
.condition .percent {
display: inline-block;
}
/* Buttons */
.unstyled-button {
background: none;
border: none;
padding: 0;
font: inherit;
color: inherit;
}
.vehicle-action {
align-self: stretch;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255,255,255,.08);
color: var(--darkest);
background: linear-gradient(180deg, #ffad2e, var(--brand));
font-weight: 700;
letter-spacing: .2px;
cursor: pointer;
transition: transform var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
}
.vehicle-action:hover {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(251,155,4,.25);
border-color: rgba(251,155,4,.55);
}
.vehicle-action.red {
color: var(--lightest);
background: linear-gradient(180deg, #d87070, #b35252);
border-color: rgba(255,255,255,.08);
}
.vehicle-action.red:hover {
box-shadow: 0 6px 18px rgba(179,82,82,.25);
border-color: rgba(179,82,82,.55);
}
.group {
margin: 20px;
color: var(--lightest);
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
border: 1px solid rgba(255,255,255,.08);
border-radius: var(--radius-md);
background: rgba(37,37,37,.7);
}
.impounded_content { display: none; }
.group > button {
color: var(--darkest);
background: var(--brand);
border: 1px solid rgba(251,155,4,.55);
border-radius: var(--radius-sm);
padding: 10px 12px;
}
.group > input {
color: var(--lightest);
width: 100%;
padding: 12px 14px;
background: var(--mid);
border: 1px solid rgba(255,255,255,.08);
border-radius: var(--radius-sm);
outline: none;
}
.group > input:focus {
border-color: rgba(251,155,4,.55);
box-shadow: 0 0 0 3px rgba(251,155,4,.12);
}
.clear-both { clear: both; }
.right-side { width: 50%; }
.webgl { position: fixed; left: 0; top: 0; outline: none; }
.logo-left {
position: absolute;
left: 1.5%;
top: 1.5%;
height: 3vh;
width: auto;
filter: drop-shadow(0 4px 14px rgba(0,0,0,.35));
user-select: none;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,275 @@
$(window).ready(function () {
window.addEventListener("message", function (event) {
let data = event.data;
if (data.showMenu) {
$("#container").css({ display: 'grid', opacity: 0 }).animate({ opacity: 1 }, 200);
$("#menu").show();
if (data.type === "impound") {
$("#header ul").hide();
} else {
$("#header ul").show();
}
if (data.vehiclesList != undefined) {
$("#container").data("spawnpoint", data.spawnPoint);
if (data.poundCost) $("#container").data("poundcost", data.poundCost);
if (data.poundCost != undefined) {
$(".content .vehicle-list").html(
getVehicles(data.locales, data.vehiclesList, data.poundCost)
);
} else {
$(".content .vehicle-list").html(
getVehicles(data.locales, data.vehiclesList)
);
}
$(".content h2").hide();
} else {
$(".content h2").show();
$(".content .vehicle-list").empty();
}
if (data.vehiclesImpoundedList != undefined) {
$(".impounded_content").data("poundName", data.poundName);
$(".impounded_content").data("poundSpawnPoint", data.poundSpawnPoint);
if (data.poundCost) $("#container").data("poundcost", data.poundCost);
$(".impounded_content .vehicle-list").html(
getImpoundedVehicles(data.locales, data.vehiclesImpoundedList)
);
$(".impounded_content h2").hide();
} else {
$(".impounded_content h2").show();
$(".impounded_content .vehicle-list").empty();
}
// Locales
// needs a rework
// $(".content h2").html(function (i, text) {
// return text.replace("No vehicle in this garage.", data.locales.no_veh_parking);
// });
// $(".impounded_content h2").html(function (i, text) {
// return text.replace("No vehicle impounded.", data.locales.no_veh_impounded);
// });
$(".vehicle-listing").html(function (i, text) {
return text.replace("Model", data.locales.veh_model);
});
$(".vehicle-listing").html(function (i, text) {
return text.replace("Plate", data.locales.veh_plate);
});
$(".vehicle-listing").html(function (i, text) {
return text.replace("Condition", data.locales.veh_condition);
});
} else if (data.hideAll) {
$("#container").stop(true, true).fadeOut(150, function(){
$(this).css('display','none');
});
}
});
$("#container").hide();
$(".close").click(function (event) {
$("#container").hide();
$.post("https://esx_garage/escape", "{}");
$(".impounded_content").hide();
$(".content").show();
$('li[data-page="garage"]').addClass("selected");
$('li[data-page="impounded"]').removeClass("selected");
});
document.onkeyup = function (data) {
if (data.which == 27) {
$.post("https://esx_garage/escape", "{}");
$(".impounded_content").hide();
$(".content").show();
$('li[data-page="garage"]').addClass("selected");
$('li[data-page="impounded"]').removeClass("selected");
}
};
function clamp(n, min, max){ return Math.max(min, Math.min(max, n)); }
function getVehicles(locale, vehicle, amount = null) {
let html = "";
let vehicleData = JSON.parse(vehicle);
let bodyHealth = 1000;
let engineHealth = 1000;
let tankHealth = 1000;
let vehicleDamagePercent = "";
for (let i = 0; i < vehicleData.length; i++) {
const p = vehicleData[i].props || {};
const b = typeof p.bodyHealth === 'number' ? p.bodyHealth : 1000;
const e = typeof p.engineHealth === 'number' ? p.engineHealth : 1000;
const t = typeof p.tankHealth === 'number' ? p.tankHealth : 1000;
bodyHealth = clamp((b / 1000) * 100, 0, 100);
engineHealth = clamp((e / 1000) * 100, 0, 100);
tankHealth = clamp((t / 1000) * 100, 0, 100);
const percentNumber = clamp(Math.round(((bodyHealth + engineHealth + tankHealth) / 300) * 100), 0, 100);
vehicleDamagePercent = percentNumber + "%";
const model = String(vehicleData[i].model || '').toLowerCase();
const plate = String(vehicleData[i].plate || '').toLowerCase();
html += "<div class='vehicle-listing' data-model='" + model + "' data-plate='" + plate + "'>";
html += "<div>Model: <strong>" + vehicleData[i].model + "</strong></div>";
html += "<div>Plate: <strong>" + vehicleData[i].plate + "</strong></div>";
html +=
"<div class='condition'><span>Condition</span><div class='bar'><div class='fill' style='width:" + vehicleDamagePercent + "'></div></div><strong class='percent'>" + vehicleDamagePercent + "</strong></div>";
html +=
"<button data-button='spawn' class='vehicle-action unstyled-button' data-vehprops='" +
JSON.stringify(vehicleData[i].props) +
"'>" +
locale.action +
(amount ? " ($" + amount + ")" : "") +
"</button>";
html += "</div>";
}
return html;
}
function getImpoundedVehicles(locale, vehicle) {
let html = "";
let vehicleData = JSON.parse(vehicle);
let bodyHealth = 1000;
let engineHealth = 1000;
let tankHealth = 1000;
let vehicleDamagePercent = "";
for (let i = 0; i < vehicleData.length; i++) {
const p = vehicleData[i].props || {};
const b = typeof p.bodyHealth === 'number' ? p.bodyHealth : 1000;
const e = typeof p.engineHealth === 'number' ? p.engineHealth : 1000;
const t = typeof p.tankHealth === 'number' ? p.tankHealth : 1000;
bodyHealth = clamp((b / 1000) * 100, 0, 100);
engineHealth = clamp((e / 1000) * 100, 0, 100);
tankHealth = clamp((t / 1000) * 100, 0, 100);
const percentNumber = clamp(Math.round(((bodyHealth + engineHealth + tankHealth) / 300) * 100), 0, 100);
vehicleDamagePercent = percentNumber + "%";
const model = String(vehicleData[i].model || '').toLowerCase();
const plate = String(vehicleData[i].plate || '').toLowerCase();
html += "<div class='vehicle-listing' data-model='" + model + "' data-plate='" + plate + "'>";
html += "<div>Model: <strong>" + vehicleData[i].model + "</strong></div>";
html += "<div>Plate: <strong>" + vehicleData[i].plate + "</strong></div>";
html +=
"<div class='condition'><span>Condition</span><div class='bar'><div class='fill' style='width:" + vehicleDamagePercent + "'></div></div><strong class='percent'>" + vehicleDamagePercent + "</strong></div>";
html +=
"<button data-button='impounded' class='vehicle-action red unstyled-button' data-vehprops='" +
JSON.stringify(vehicleData[i].props) +
"'>" +
locale.impound_action +
"</button>";
html += "</div>";
}
return html;
}
function applyFilter($container, query) {
const q = String(query || '').trim().toLowerCase();
const $cards = $container.find('.vehicle-listing');
if (!q) {
$cards.show();
return;
}
$cards.each(function () {
const model = String($(this).data('model') || '');
const plate = String($(this).data('plate') || '');
if (model.indexOf(q) !== -1 || plate.indexOf(q) !== -1) {
$(this).show();
} else {
$(this).hide();
}
});
}
$(document).on('input', '#search-garage', function () {
applyFilter($('.content .vehicle-list'), $(this).val());
});
$(document).on('input', '#search-impounded', function () {
applyFilter($('.impounded_content .vehicle-list'), $(this).val());
});
// Helpers (none for now)
$('li[data-page="garage"]').click(function (event) {
$(".impounded_content").hide();
$(".content").show();
$('li[data-page="garage"]').addClass("selected");
$('li[data-page="impounded"]').removeClass("selected");
});
$('li[data-page="impounded"]').click(function (event) {
$(".content").hide();
$(".impounded_content").show();
$('li[data-page="impounded"]').addClass("selected");
$('li[data-page="garage"]').removeClass("selected");
});
$(document).on(
"click",
"button[data-button='spawn'].vehicle-action",
function (event) {
let spawnPoint = $("#container").data("spawnpoint");
let poundCost = $("#container").data("poundcost");
let vehicleProps = $(this).data("vehprops");
// prevent empty cost
if (poundCost === undefined) poundCost = 0;
$.post(
"https://esx_garage/spawnVehicle",
JSON.stringify({
vehicleProps: vehicleProps,
spawnPoint: spawnPoint,
exitVehicleCost: poundCost,
})
);
$(".impounded_content").hide();
$(".content").show();
$('li[data-page="garage"]').addClass("selected");
$('li[data-page="impounded"]').removeClass("selected");
}
);
$(document).on(
"click",
"button[data-button='impounded'].vehicle-action",
function (event) {
let vehicleProps = $(this).data("vehprops");
let poundName = $(".impounded_content").data("poundName");
let poundSpawnPoint = $(".impounded_content").data("poundSpawnPoint");
$.post(
"https://esx_garage/impound",
JSON.stringify({
vehicleProps: vehicleProps,
poundName: poundName,
poundSpawnPoint: poundSpawnPoint,
})
);
$(".impounded_content").hide();
$(".content").show();
$('li[data-page="garage"]').addClass("selected");
$('li[data-page="impounded"]').removeClass("selected");
}
);
});

Binary file not shown.

View File

@@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ESX Garage</title>
<link rel="stylesheet" href="css/app.css" type="text/css" />
</head>
<body>
<div id="container" role="dialog" style="display:none;">
<div id="header">
<div class="title" id="garage-title">Garage</div>
<ul role="tablist">
<li data-page="garage" class="selected" role="tab">Garage</li>
<li data-page="impounded" role="tab">Impounded</li>
</ul>
<img class="logo-left" src="./img/logo.png" alt="ESX logo"/>
<button class="close" title="Close">×</button>
</div>
<div id="menu">
<div class="content" role="tabpanel">
<div class="toolbar">
<input id="search-garage" type="search" placeholder="Search by model or plate…" />
</div>
<h2 class="empty">No Vehicles Stored Here.</h2>
<div class="vehicle-list"></div>
</div>
<div class="impounded_content" role="tabpanel">
<div class="toolbar">
<input id="search-impounded" type="search" placeholder="Search by model or plate…" />
</div>
<h2 class="empty">No Impounded Vehicles.</h2>
<div class="vehicle-list"></div>
</div>
</div>
</div>
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
<script src="js/app.js"></script>
</body>
</html>