client.lua
ESX = nil
local display = false
local markers = {}
-- Initiera ESX
Citizen.CreateThread(function()
while ESX == nil do
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
Citizen.Wait(10)
end
end)
-- Visa / Dölj NUI
function SetDisplay(bool)
display = bool
SetNuiFocus(bool, bool)
SendNUIMessage({
action = "setVisible",
visible = bool
})
end
-- Event för att öppna UI
RegisterNetEvent('privatinkop:openUI')
AddEventHandler('privatinkop:openUI', function()
SetDisplay(true)
end)
-- NUI callback för att stänga UI
RegisterNUICallback('close', function(data, cb)
SetDisplay(false)
TriggerEvent('esx:showNotification', 'Stängde Privat Inköp')
cb('ok')
end)
-- NUI notify (för felmeddelanden mm)
RegisterNUICallback('notify', function(data, cb)
local msg = data.message or "Okänt fel"
TriggerEvent('esx:showNotification', msg)
cb('ok')
end)
-- Testkommando för att öppna UI
RegisterCommand('privatinkop', function()
TriggerEvent('privatinkop:openUI')
end)
-- NUI callback för att hämta coords till NUI (om du behöver det)
RegisterNUICallback("getCoords", function(data, cb)
local playerPed = PlayerPedId()
local coords = GetEntityCoords(playerPed)
local coordsString = string.format("%.2f, %.2f, %.2f", coords.x, coords.y, coords.z)
SendNUIMessage({
action = "setCoords",
coords = coordsString
})
cb("ok")
end)
-- NUI callback för att spara koordinater, skickar till servern
RegisterNUICallback("sparaKoordinater", function(data, cb)
if data.x and data.y and data.z then
TriggerServerEvent("privatinkop:sparaKoordinater", {
x = tonumber(data.x),
y = tonumber(data.y),
z = tonumber(data.z)
})
else
TriggerEvent('esx:showNotification', 'Fel vid sparande av koordinater.')
end
cb("ok")
end)
-- Event för att ta emot markörer från servern
RegisterNetEvent("privatinkop:läggTillMarker")
AddEventHandler("privatinkop:läggTillMarker", function(coords)
table.insert(markers, vector3(coords.x, coords.y, coords.z))
print(("Ny markör tillagd: %.2f, %.2f, %.2f"):format(coords.x, coords.y, coords.z))
end)
-- Loop som ritar markörer och hanterar input nära markörerna
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
local playerCoords = GetEntityCoords(PlayerPedId())
for _, coord in ipairs(markers) do
DrawMarker(
1, -- Cylinder marker
coord.x, coord.y, coord.z - 1.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1.0, 1.0, 1.0,
0, 255, 0, 200, -- Grön färg
false, true, 2, false, nil, nil, false
)
local dist = #(playerCoords - coord)
if dist < 2.0 then
ESX.ShowHelpNotification("Tryck ~INPUT_CONTEXT~ (E) för att öppna Privat Inköp")
if IsControlJustReleased(0, 38) then -- 38 = E
TriggerEvent('privatinkop:openUI')
end
end
end
end
end)
config.lua
Config = {}
-- Lägg till Steam Hexs för de som ska kunna använda /privatinkop
Config.AllowedSteamHex = {
"steam:110000112345678", -- exempel hex
"steam:110000112345679"
}
server.lua
local savedCoords = {}
-- Kontrollera om spelaren har behörighet att använda kommandot
RegisterCommand('privatinkop', function(source, args, rawCommand)
local src = source
local steamHex = GetPlayerIdentifiers(src)[1] -- steam hex är alltid första identifieraren
local allowed = false
for _, hex in pairs(Config.AllowedSteamHex) do
if hex == steamHex then
allowed = true
break
end
end
if allowed then
-- Skicka event till klienten att öppna UI
TriggerClientEvent('privatinkop:openUI', src)
else
TriggerClientEvent('chat:addMessage', src, {
args = { "^1SYSTEM", "Du har inte behörighet att använda detta kommando." }
})
end
end)
-- Event som hanterar sparandet av koordinater från klienten
RegisterNetEvent("privatinkop:sparaKoordinater")
AddEventHandler("privatinkop:sparaKoordinater", function(data)
local src = source
if data and data.x and data.y and data.z then
-- Spara i tabell med key = player source, value = coords
savedCoords[src] = {x = tonumber(data.x), y = tonumber(data.y), z = tonumber(data.z)}
print(("[PrivatInköp] Sparade coords från spelare %d: %.2f, %.2f, %.2f"):format(src, data.x, data.y, data.z))
-- Skicka coords tillbaka till just den spelaren för att visa markör
TriggerClientEvent("privatinkop:läggTillMarker", src, savedCoords[src])
else
print("[PrivatInköp] Felaktiga koordinater från spelare " .. src)
end
end)
-- (Valfritt) Kommando för att skicka alla sparade coords till en spelare (exempelvis admin)
RegisterCommand('visaMarkers', function(source, args, rawCommand)
local src = source
for _, coords in pairs(savedCoords) do
TriggerClientEvent("privatinkop:läggTillMarker", src, coords)
end
TriggerClientEvent('chat:addMessage', src, {
args = { "^2SYSTEM", "Visar alla sparade markörer." }
})
end)
script.js
document.addEventListener('DOMContentLoaded', () => {
const appContainer = document.getElementById('appContainer');
function setAppVisible(show) {
appContainer.style.display = show ? 'flex' : 'none';
}
window.setAppVisible = setAppVisible;
const RES = typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'privatinkop';
function nuiPost(name, payload = {}) {
fetch(`https://${RES}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(payload)
}).catch(() => {});
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
setAppVisible(false);
nuiPost('close');
}
});
window.addEventListener('message', (event) => {
const data = event.data || {};
if (data.action === 'setVisible') setAppVisible(!!data.visible);
if (data.action === 'toggle') setAppVisible(appContainer.style.display === 'none');
if (data.action === 'setCoords') {
const createCoords = document.getElementById('createCoords');
createCoords.value = data.coords;
}
});
const purchaseList = document.getElementById('purchaseList');
const emptyMessage = document.getElementById('emptyMessage');
const searchInput = document.getElementById('searchInput');
const btnOpenCreateModal = document.getElementById('btnOpenCreateModal');
const createModalBackdrop = document.getElementById('createModalBackdrop');
const createModal = document.getElementById('createModal');
const btnCancelCreate = document.getElementById('btnCancelCreate');
const btnSaveCreate = document.getElementById('btnSaveCreate');
const createName = document.getElementById('createName');
const createWeaponName = document.getElementById('createWeaponName');
const createDisplayName = document.getElementById('createDisplayName');
const createPrice = document.getElementById('createPrice');
const createCoords = document.getElementById('createCoords');
const btnGetCoords = document.getElementById('btnGetCoords');
const createSteamHex = document.getElementById('createSteamHex');
const editModalBackdrop = document.getElementById('editModalBackdrop');
const editModal = document.getElementById('editModal');
const btnCancelEdit = document.getElementById('btnCancelEdit');
const btnSaveEdit = document.getElementById('btnSaveEdit');
const editWeaponName = document.getElementById('editWeaponName');
const editDisplayName = document.getElementById('editDisplayName');
const editPrice = document.getElementById('editPrice');
const editSteamHex = document.getElementById('editSteamHex');
let purchases = [];
let currentEditIndex = null;
function showModal(backdrop, modal) {
backdrop.classList.add('show');
setTimeout(() => modal.classList.add('show'), 10);
}
function hideModal(backdrop, modal) {
modal.classList.remove('show');
setTimeout(() => backdrop.classList.remove('show'), 300);
}
btnOpenCreateModal.addEventListener('click', () => {
createName.value = '';
createWeaponName.value = '';
createDisplayName.value = '';
createPrice.value = '';
createCoords.value = '';
createSteamHex.value = '';
showModal(createModalBackdrop, createModal);
});
btnCancelCreate.addEventListener('click', () => {
hideModal(createModalBackdrop, createModal);
});
btnSaveCreate.addEventListener('click', () => {
if (!createName.value.trim()) {
nuiPost('notify', { message: 'Du måste ange ett namn!' });
return;
}
const newPurchase = {
name: createName.value.trim(),
weaponName: createWeaponName.value.trim(),
displayName: createDisplayName.value.trim(),
price: createPrice.value.trim() || 0,
coords: createCoords.value.trim(),
steamHex: createSteamHex.value.trim()
};
purchases.push(newPurchase);
renderPurchases(searchInput.value);
hideModal(createModalBackdrop, createModal);
});
btnGetCoords.addEventListener('click', () => {
btnGetCoords.disabled = true;
btnGetCoords.textContent = '⏳';
fetch(`https://${RES}/getCoords`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify({})
}).finally(() => {
btnGetCoords.disabled = false;
btnGetCoords.textContent = '🗺️';
});
});
function renderPurchases(filter = '') {
purchaseList.innerHTML = '';
const filtered = purchases
.map((purchase, index) => ({ ...purchase, index }))
.filter(p => {
const text = `${p.name} ${p.weaponName} ${p.displayName} ${p.steamHex}`.toLowerCase();
return text.includes(filter.toLowerCase());
});
if (filtered.length === 0) {
emptyMessage.style.display = 'flex';
return;
}
emptyMessage.style.display = 'none';
filtered.forEach((p) => {
const div = document.createElement('div');
div.className = 'purchase-item';
div.innerHTML = `
<div class="name">${p.name} <div class="icon"></div></div>
<div class="details">
Vapen: ${p.weaponName}<br />
Visningsnamn: ${p.displayName}<br />
Pris: ${p.price} kr<br />
Steam HEX: ${p.steamHex}<br />
${p.coords ? `Koordinater: ${p.coords}` : ''}
</div>
<div class="actions">
<button class="btn-edit" data-index="${p.index}">Redigera</button>
<button class="btn-delete" data-index="${p.index}">Ta bort</button>
</div>
`;
purchaseList.appendChild(div);
});
// Lägg till eventlisteners för edit och delete EFTER rendering
document.querySelectorAll('.btn-edit').forEach(button => {
button.addEventListener('click', e => {
currentEditIndex = parseInt(e.target.dataset.index, 10);
const p = purchases[currentEditIndex];
if (!p) return;
editWeaponName.value = p.weaponName;
editDisplayName.value = p.displayName;
editPrice.value = p.price;
editSteamHex.value = p.steamHex;
showModal(editModalBackdrop, editModal);
});
});
document.querySelectorAll('.btn-delete').forEach(button => {
button.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const idx = parseInt(e.target.dataset.index, 10);
console.log('Trying to delete idx:', idx);
if (Number.isInteger(idx) && purchases[idx]) {
if (window.confirm('Vill du verkligen ta bort detta inköp?')) {
purchases.splice(idx, 1);
console.log('Deleted index:', idx);
console.log('Remaining purchases:', purchases);
renderPurchases(searchInput.value);
} else {
console.log('User cancelled deletion');
}
} else {
console.log('Invalid index or item does not exist');
}
});
});
}
fetch(`https://${GetParentResourceName()}/sparaKoordinater`, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify({
x: coords.x,
y: coords.y,
z: coords.z
})
})
searchInput.addEventListener('input', () => {
renderPurchases(searchInput.value);
});
btnCancelEdit.addEventListener('click', () => {
hideModal(editModalBackdrop, editModal);
});
btnSaveEdit.addEventListener('click', () => {
if (currentEditIndex === null) return;
purchases[currentEditIndex].weaponName = editWeaponName.value.trim();
purchases[currentEditIndex].displayName = editDisplayName.value.trim();
purchases[currentEditIndex].price = editPrice.value.trim() || 0;
purchases[currentEditIndex].steamHex = editSteamHex.value.trim();
renderPurchases(searchInput.value);
hideModal(editModalBackdrop, editModal);
});
// Initial render
renderPurchases();
});
style.css
@import url('https://fonts.googleapis.com/css2?family=Segoe+UI:wght@600&display=swap');
body {
background-color: transparent;
margin: 0;
font-family: 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Appen är gömd tills du visar den via JS */
#appContainer {
display: none;
}
.container {
width: 900px;
height: 530px;
background-color: #1a3d7a;
border-radius: 6px;
box-shadow: 0 0 15px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
}
.header {
height: 60px;
background-color: #2353a1;
display: flex;
align-items: center;
padding: 0 24px;
color: white;
font-weight: 600;
font-size: 20px;
gap: 10px;
}
.icon-store {
width: 25px;
height: 25px;
background: white;
mask: url('data:image/svg+xml;utf8,<svg fill="black" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 9v11h18V9H3zm16 9H5v-7h14v7zm-1-16H6v3h12V2z"/></svg>') no-repeat center;
mask-size: contain;
}
.icon-diamond {
margin-left: auto;
width: 30px;
height: 30px;
background: #3b6ecc;
mask: url('data:image/svg+xml;utf8,<svg fill="black" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l-8 8 8 12 8-12-8-8z"/></svg>') no-repeat center;
mask-size: contain;
}
.content {
flex-grow: 1;
padding: 18px 24px 24px 24px;
display: flex;
flex-direction: column;
}
.search-row {
display: flex;
gap: 10px;
}
.search-row input {
flex-grow: 1;
background-color: #1a3d7a;
border: 2px solid #4e75ca;
border-radius: 6px;
padding: 10px 14px;
font-weight: 600;
font-size: 16px;
color: white;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
.search-row input::placeholder {
color: #8c9dbc;
font-weight: 600;
font-size: 15px;
}
.search-row input:focus {
outline: none;
border-color: #3db855;
background-color: #164076;
}
.search-row button {
background-color: #4e75ca;
font-weight: 600;
font-size: 15px;
height: 42px;
width: 140px;
border-radius: 6px;
border: none;
cursor: pointer;
color: white;
transition: background-color 0.3s ease;
text-transform: uppercase;
}
.search-row button:hover {
background-color: #3b5bbf;
}
.empty-message {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
color: #8c9dbc;
font-size: 19px;
text-align: center;
user-select: none;
}
.purchase-list {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.purchase-item {
background-color: #1f3b82;
border-radius: 8px;
padding: 14px;
color: white;
margin-bottom: 16px;
width: 250px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.purchase-item .name {
font-weight: 700;
font-size: 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.purchase-item .name .icon {
background-image: url('data:image/svg+xml;utf8,<svg fill="white" height="16" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M12 12c2.7 0 8 1.3 8 4v2H4v-2c0-2.7 5.3-4 8-4zm0-2c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/></svg>');
background-size: contain;
width: 16px;
height: 16px;
}
.purchase-item .details {
font-size: 13px;
line-height: 1.4;
margin-bottom: 12px;
}
.purchase-item .actions {
display: flex;
gap: 8px;
}
.purchase-item .actions button {
padding: 6px 10px;
background-color: #2c57d0;
color: white;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.purchase-item .actions button:hover {
background-color: #3e6ef3;
}
/* Modal backdrop */
.modal-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(26, 61, 122, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal-backdrop.show {
display: flex;
opacity: 1;
}
.modal {
width: 450px; /* Lite mindre bredd som i referensbild */
background-color: #1a3d7a;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
padding: 20px;
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.modal.show {
opacity: 1;
transform: translateY(0);
}
.modal-header {
font-weight: 600;
font-size: 18px;
color: white;
margin-bottom: 12px;
}
.modal-content label {
font-size: 13px;
color: #8c9dbc;
margin-bottom: 5px;
font-weight: 600;
}
.modal-content input[type="text"],
.modal-content input[type="number"] {
width: 100%;
border: 2px solid #4e75ca;
border-radius: 6px;
padding: 10px 14px;
font-weight: 600;
font-size: 15px;
color: white;
background-color: #164076;
transition: border-color 0.3s ease;
box-sizing: border-box;
margin-bottom: 12px;
}
.modal-content input[type="text"]::placeholder,
.modal-content input[type="number"]::placeholder {
color: #8c9dbc;
font-weight: 600;
font-size: 14px;
}
.modal-content input[type="text"]:focus,
.modal-content input[type="number"]:focus {
outline: none;
border-color: #3db855;
background-color: #164076;
}
/* Rad med flera inputs (Tillgängliga varor) */
.flex-row {
display: flex;
gap: 10px;
margin-top: 10px;
}
.flex-row input[type="text"],
.flex-row input[type="number"] {
border-radius: 6px;
font-size: 14px;
padding: 8px 12px;
}
.flex-row input[type="text"]:nth-child(1) {
flex: 2; /* större */
}
.flex-row input[type="text"]:nth-child(2) {
flex: 3; /* lite större */
}
.flex-row input[type="number"] {
flex: 1;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
.btn-cancel {
font-weight: 600;
font-size: 14px;
color: #ff5555;
background: transparent;
border: none;
cursor: pointer;
letter-spacing: 1.5px;
padding: 6px 10px;
transition: background-color 0.3s ease;
}
.btn-cancel:hover {
background-color: rgba(255, 85, 85, 0.1);
border-radius: 6px;
}
.btn-save {
font-weight: 600;
font-size: 14px;
padding: 8px 20px;
border-radius: 6px;
background-color: #7eea28; /* grön */
border: none;
color: black;
cursor: pointer;
letter-spacing: 1.5px;
transition: background-color 0.3s ease;
}
.btn-save:hover {
background-color: #5ec21f;
}
/* Placera "Steam ID" input ovanför tillgängliga varor */
.steam-id-input {
width: 100%;
margin-bottom: 15px;
padding: 10px 14px;
border-radius: 6px;
border: 2px solid #4e75ca;
font-weight: 600;
font-size: 15px;
background-color: #164076;
color: white;
box-sizing: border-box;
}
.steam-id-input::placeholder {
color: #8c9dbc;
font-weight: 600;
font-size: 14px;
}
.steam-id-input:focus {
outline: none;
border-color: #3db855;
background-color: #164076;
}
.btn-cancel {
font-weight: 600;
font-size: 16px;
color: #ff5555;
background: transparent;
border: none;
cursor: pointer;
letter-spacing: 1.5px;
}
index.html
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8" />
<title>Privatinköp med lista och redigering</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Själva appen (gömd som standard via CSS) -->
<div class="container" id="appContainer">
<div class="header">
<div class="icon-store"></div>
PRIVATINKÖP
<div class="icon-diamond"></div>
</div>
<div class="content">
<div class="search-row">
<input type="text" id="searchInput" placeholder="Sök efter inköp..." />
<button id="btnOpenCreateModal">SKAPA INKÖP</button>
</div>
<div class="purchase-list" id="purchaseList"></div>
<div class="empty-message" id="emptyMessage">Inga privata inköp hittades</div>
</div>
</div>
<!-- Skapa modal -->
<div class="modal-backdrop" id="createModalBackdrop">
<div class="modal" id="createModal">
<div class="modal-content">
<label for="createName">Namn</label>
<input type="text" id="createName" placeholder="Namn" />
<label for="createWeaponName">Vapen namn</label>
<input type="text" id="createWeaponName" placeholder="Vapen namn" />
<label for="createDisplayName">Visningsnamn</label>
<input type="text" id="createDisplayName" placeholder="Visningsnamn" />
<label for="createPrice">Pris</label>
<input type="number" id="createPrice" placeholder="Pris" min="0" />
<label for="createCoords">Koordinater</label>
<div style="display:flex; gap:8px; align-items:center;">
<input type="text" id="createCoords" placeholder="X, Y, Z" />
<button type="button" id="btnGetCoords" title="Hämta nuvarande position">🗺️</button>
</div>
<label for="createSteamHex">Steam HEX</label>
<input type="text" id="createSteamHex" placeholder="Steam HEX" maxlength="17" />
</div>
<div class="modal-footer">
<button class="btn-cancel" id="btnCancelCreate">Avbryt</button>
<button class="btn-save" id="btnSaveCreate">Skapa</button>
</div>
</div>
</div>
<!-- Redigera modal -->
<div class="modal-backdrop" id="editModalBackdrop">
<div class="modal" id="editModal">
<div class="modal-content">
<label for="editWeaponName">Vapen namn</label>
<input type="text" id="editWeaponName" placeholder="Vapen namn" />
<label for="editDisplayName">Visningsnamn</label>
<input type="text" id="editDisplayName" placeholder="Visningsnamn" />
<label for="editPrice">Pris</label>
<input type="number" id="editPrice" placeholder="Pris" min="0" />
<label for="editSteamHex">Steam HEX</label>
<input type="text" id="editSteamHex" placeholder="Steam HEX" maxlength="17" />
</div>
<div class="modal-footer">
<button class="btn-cancel" id="btnCancelEdit">Avbryt</button>
<button class="btn-save" id="btnSaveEdit">Spara</button>
</div>
</div>
</div>
<script src="script.js" defer></script>
</body>
</html>
Fixa alla fel errros osv jag har nyaste esx legacy , Fixa allt som e fel sen fixa ist för vanliga ikomner det ska vara mdi ikoner