From d52c66b397808ddb47da9614516e6e1235cad48e Mon Sep 17 00:00:00 2001 From: SyncOusli <142453413+SyncOusli@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:26:39 +0100 Subject: [PATCH 1/2] Refactor shop system to use NUI for UI interactions, update item configuration, and enhance localization support - Replaced the previous shop menu system with a NUI-based interface for better user experience. - Updated the configuration to include item categories and improved item data structure. - Enhanced localization files for multiple languages, adding new keys for better user feedback. - Removed deprecated marker and blip logic, simplifying the shop interaction process. - Added commands and key mappings for toggling the shop UI. --- [esx_addons]/esx_shops/client/main.lua | 205 +++++++++++------------ [esx_addons]/esx_shops/config.lua | 112 +++---------- [esx_addons]/esx_shops/fxmanifest.lua | 11 +- [esx_addons]/esx_shops/locales/de.lua | 27 ++- [esx_addons]/esx_shops/locales/en.lua | 27 ++- [esx_addons]/esx_shops/locales/es.lua | 27 ++- [esx_addons]/esx_shops/locales/fi.lua | 27 ++- [esx_addons]/esx_shops/locales/fr.lua | 27 ++- [esx_addons]/esx_shops/locales/hu.lua | 28 ++-- [esx_addons]/esx_shops/locales/it.lua | 27 ++- [esx_addons]/esx_shops/locales/nl.lua | 27 ++- [esx_addons]/esx_shops/locales/pl.lua | 27 ++- [esx_addons]/esx_shops/locales/sl.lua | 27 ++- [esx_addons]/esx_shops/locales/sr.lua | 27 ++- [esx_addons]/esx_shops/locales/sv.lua | 31 ++-- [esx_addons]/esx_shops/locales/tr.lua | 27 ++- [esx_addons]/esx_shops/locales/zh-cn.lua | 27 ++- [esx_addons]/esx_shops/server/main.lua | 76 ++++++--- 18 files changed, 356 insertions(+), 431 deletions(-) diff --git a/[esx_addons]/esx_shops/client/main.lua b/[esx_addons]/esx_shops/client/main.lua index 59c5a1db..3f1aa5fb 100644 --- a/[esx_addons]/esx_shops/client/main.lua +++ b/[esx_addons]/esx_shops/client/main.lua @@ -1,121 +1,116 @@ -local hasAlreadyEnteredMarker, lastZone -local currentAction, currentActionMsg, currentActionData = nil, nil, {} - -local function openShopMenu(zone) - local elements = { - {unselectable = true, icon = "fas fa-shopping-basket", title = TranslateCap('shop') } - } - - for i=1, #Config.Zones[zone].Items, 1 do - local item = Config.Zones[zone].Items[i] - - elements[#elements+1] = { - icon = "fas fa-shopping-basket", - title = ('%s - %s'):format(item.label, TranslateCap('shop_item', ESX.Math.GroupDigits(item.price))), - itemLabel = item.label, - item = item.name, - price = item.price - } - end - - ESX.OpenContext("right", elements, function(menu,element) - local elements2 = { - {unselectable = true, icon = "fas fa-shopping-basket", title = element.title}, - {icon = "fas fa-shopping-basket", title = TranslateCap('amount'), input = true, inputType = "number", inputPlaceholder = TranslateCap('amount_placeholder'), inputMin = 1, inputMax = 25}, - {icon = "fas fa-check-double", title = TranslateCap('confirm'), val = "confirm"} - } - - ESX.OpenContext("right", elements2, function(menu2,element2) - local amount = menu2.eles[2].inputValue - ESX.CloseContext() - TriggerServerEvent('esx_shops:buyItem', element.item, amount, zone) - end, function(menu) - currentAction = 'shop_menu' - currentActionMsg = TranslateCap('press_menu', ESX.GetInteractKey()) - currentActionData = {zone = zone} - end) - end, function(menu) - currentAction = 'shop_menu' - currentActionMsg = TranslateCap('press_menu', ESX.GetInteractKey()) - currentActionData = {zone = zone} - end) +---@diagnostic disable: undefined-global +local ESX = exports['es_extended']:getSharedObject() +local isShopUiOpen = false + +local function setShopUiVisible(visible) + isShopUiOpen = visible + SetNuiFocus(visible, visible) + SendNUIMessage({ action = visible and 'show' or 'hide' }) end -local function hasEnteredMarker(zone) - currentAction = 'shop_menu' - currentActionMsg = TranslateCap('press_menu', ESX.GetInteractKey()) - currentActionData = {zone = zone} +local function openShopUi() + if isShopUiOpen then return end + setShopUiVisible(true) end -local function hasExitedMarker(zone) - currentAction = nil - ESX.CloseContext() +local function closeShopUi() + if not isShopUiOpen then return end + setShopUiVisible(false) end --- Create Blips -CreateThread(function() - for k,v in pairs(Config.Zones) do - for i = 1, #v.Pos, 1 do - if not v.ShowBlip then return end - - local blip = AddBlipForCoord(v.Pos[i]) +RegisterCommand('shopui', function() + if isShopUiOpen then + closeShopUi() + else + openShopUi() + end +end, false) - SetBlipSprite (blip, v.Type) - SetBlipScale (blip, v.Size) - SetBlipColour (blip, v.Color) - SetBlipAsShortRange(blip, true) +RegisterKeyMapping('shopui', 'Toggle Shop UI', 'keyboard', 'F7') - BeginTextCommandSetBlipName('STRING') - AddTextComponentSubstringPlayerName(TranslateCap('shops')) - EndTextCommandSetBlipName(blip) - end - end +RegisterNUICallback('close', function(_, cb) + closeShopUi() + if cb then cb({}) end end) --- Enter / Exit marker events -CreateThread(function() - while true do - local sleep = 1500 +AddEventHandler('onResourceStop', function(resourceName) + if GetCurrentResourceName() ~= resourceName then return end + SetNuiFocus(false, false) +end) - local playerCoords = GetEntityCoords(ESX.PlayerData.ped) - local isInMarker, currentZone = false, nil - for k,v in pairs(Config.Zones) do - for i = 1, #v.Pos, 1 do - local distance = #(playerCoords - v.Pos[i]) +RegisterNUICallback('getShopData', function(_, cb) + cb({ + categories = Config.Categories or {}, + items = Config.Items or {} + }) +end) - if distance < Config.DrawDistance then - sleep = 0 - if v.ShowMarker then - DrawMarker(Config.MarkerType, v.Pos[i], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Config.MarkerSize.x, Config.MarkerSize.y, Config.MarkerSize.z, Config.MarkerColor.r, Config.MarkerColor.g, Config.MarkerColor.b, 100, false, true, 2, false, nil, nil, false) - end - if distance < 2.0 then - isInMarker = true - currentZone = k - lastZone = k - end - end - end - end +RegisterNUICallback('purchase', function(data, cb) + local payload = data or {} + ESX.TriggerServerCallback('esx_shops:purchase', function(success, message) + cb({ success = success, message = message }) + if success then + ESX.ShowNotification(message) + else + ESX.ShowNotification(message) + end + end, payload) +end) - if isInMarker and not hasAlreadyEnteredMarker then - hasAlreadyEnteredMarker = true - hasEnteredMarker(currentZone) - ESX.TextUI(currentActionMsg) - end +---@param label any +local function showHelpText(label) + BeginTextCommandDisplayHelp('STRING') + AddTextComponentSubstringPlayerName(label) + EndTextCommandDisplayHelp(0, false, true, 1) +end - if not isInMarker and hasAlreadyEnteredMarker then - hasAlreadyEnteredMarker = false - ESX.HideUI() - hasExitedMarker(lastZone) - end - - Wait(sleep) - end +CreateThread(function() + while true do + local playerPed = PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) + local nearby = false + local inInteractRange = false + + for _, loc in ipairs(Config.Locations or {}) do + local coords = loc.coords + local distance = #(playerCoords - coords) + if distance < 20.0 then + nearby = true + if loc.marker ~= false then + DrawMarker( + 2, + coords.x, coords.y, coords.z, + 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, + 0.2, 0.2, 0.2, + 204, 153, 0, 200, + false, true, 2, true, nil, nil, false + ) + end + if distance < 1.8 then + inInteractRange = true + if not isShopUiOpen then + local helpText = 'Press ~INPUT_CONTEXT~ to open shop' + if type(_U) == 'function' then + local translated = _U('open_shop_help') + if type(translated) == 'string' and translated ~= '' then + helpText = translated + end + end + showHelpText(helpText) + end + if IsControlJustPressed(0, 38) then -- INPUT_PICKUP (E) + openShopUi() + end + end + end + end + + if nearby then + Wait(0) + else + Wait(500) + end + end end) - -ESX.RegisterInteraction("shop_menu", function() - openShopMenu(currentActionData.zone) -end, function() - return currentAction and currentAction == 'shop_menu' -end) \ No newline at end of file diff --git a/[esx_addons]/esx_shops/config.lua b/[esx_addons]/esx_shops/config.lua index 9468821b..39d54b68 100644 --- a/[esx_addons]/esx_shops/config.lua +++ b/[esx_addons]/esx_shops/config.lua @@ -1,98 +1,28 @@ Config = {} -Config.DrawDistance = 7.5 -Config.MarkerSize = {x = 1.1, y = 0.7, z = 1.1} -Config.MarkerType = 29 -Config.MarkerColor = {r = 50, g = 200, b = 50, a = 200} -Config.Locale = GetConvar('esx:locale', 'en') -Config.Zones = { +Config.Locale = GetConvar('esx:locale', 'fr') - TwentyFourSeven = { - Items = { - { - name = "bread", - label = TranslateCap('bread'), - price = 100 - }, - { - name = "water", - label = TranslateCap('water'), - price = 100 - } - }, - Pos = { - vector3(373.8, 325.8, 103.5), - vector3(2557.4, 382.2, 108.6), - vector3(-3038.9, 585.9, 7.9), - vector3(-3241.9, 1001.4, 12.8), - vector3(547.4, 2671.7, 42.1), - vector3(1961.4, 3740.6, 32.3), - vector3(2678.9, 3280.6, 55.2), - vector3(1729.2, 6414.1, 35.0) - }, - Size = 0.8, - Type = 59, - Color = 25, - ShowBlip = true, - ShowMarker = true -}, +Config.ItemMaxQuantity = 100 - RobsLiquor = { - Items = { - { - name = "bread", - label = TranslateCap('bread'), - price = 100 - }, - { - name = "water", - label = TranslateCap('water'), - price = 100 - } - }, - Pos = { - vector3(1135.8, -982.2, 46.4), - vector3(-1222.9, -906.9, 12.3), - vector3(-1487.5, -379.1, 40.1), - vector3(-2968.2, 390.9, 15.0), - vector3(1166.0, 2708.9, 38.1), - vector3(1392.5, 3604.6, 34.9), - vector3(127.8, -1284.7, 29.2), --StripClub - vector3(-1393.4, -606.6, 30.3), --Tequila la - vector3(-559.9, 287.0, 82.1) --Bahamamas - }, - Size = 0.8, - Type = 59, - Color = 25, - ShowBlip = true, - ShowMarker = true -}, - - LTDgasoline = { - Items = { - { - name = "bread", - label = TranslateCap('bread'), - price = 100 - }, - { - name = "water", - label = TranslateCap('water'), - price = 100 - } - }, - Pos = { - vector3(-48.5, -1757.5, 29.4), - vector3(1163.3, -323.8, 69.2), - vector3(-707.5, -914.2, 19.2), - vector3(-1820.5, 792.5, 138.1), - vector3(1698.3, 4924.4, 42.0) - }, - Size = 0.8, - Type = 59, - Color = 25, - ShowBlip = true, - ShowMarker = true +Config.Categories = { + { id = 'drinks', label = 'Drinks' }, + { id = 'food', label = 'Food' }, + { id = 'essentials', label = 'Essentials' }, + { id = 'others', label = 'Others' } } + +Config.Items = { + { name = 'water', label = 'Water', price = 200, category = 'drinks', amount = 1, image = 'https://r2.fivemanage.com/R92pivz8ZlXwjJjTvi3Oq/water.png' }, + { name = 'sandwich', label = 'Sandwich', price = 500, category = 'drinks', amount = 1, image = 'https://r2.fivemanage.com/R92pivz8ZlXwjJjTvi3Oq/sprunk.png' }, + { name = 'donut', label = 'Donut', price = 50, category = 'food', amount = 1, image = 'https://r2.fivemanage.com/R92pivz8ZlXwjJjTvi3Oq/donut.png' }, + { name = 'pizza', label = 'Pizza', price = 9900, category = 'food', amount = 1, image = 'https://r2.fivemanage.com/R92pivz8ZlXwjJjTvi3Oq/pizza_ham_slice.png' }, + { name = 'lockpick', label = 'Lockpick', price = 2500, category = 'essentials', amount = 1, image = 'https://r2.fivemanage.com/R92pivz8ZlXwjJjTvi3Oq/lockpick.png' } } +-- Shop locations. Add as many as you want. +-- Each entry: { coords = vector3(x, y, z), marker = true/false, text = 'Display name' } +Config.Locations = { + { coords = vector3(25.740662, -1347.652710, 29.482056), marker = true, text = 'Shop' }, + { coords = vector3(25.767035, -1345.173584, 29.482056), marker = true, text = 'Shop' }, + -- you can add more locations as you want +} diff --git a/[esx_addons]/esx_shops/fxmanifest.lua b/[esx_addons]/esx_shops/fxmanifest.lua index 5e26dd2b..30387906 100644 --- a/[esx_addons]/esx_shops/fxmanifest.lua +++ b/[esx_addons]/esx_shops/fxmanifest.lua @@ -5,7 +5,6 @@ game 'gta5' description 'A shop system for ESX Legacy, to allow players to buy items' lua54 'yes' version '1.2' -legacyversion '1.13.4' shared_script '@es_extended/imports.lua' @@ -25,3 +24,13 @@ server_scripts { } dependency 'es_extended' + +ui_page { + 'html/index.html', +} + +files { + 'html/index.html', + 'html/app.js', + 'html/style.css', +} diff --git a/[esx_addons]/esx_shops/locales/de.lua b/[esx_addons]/esx_shops/locales/de.lua index 570d3bc0..b5ffff8f 100644 --- a/[esx_addons]/esx_shops/locales/de.lua +++ b/[esx_addons]/esx_shops/locales/de.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['de'] = { ['shop'] = 'Shop', - ['shops'] = 'Shops', - ['press_menu'] = 'Drücke [%s] um auf den ~g~Shop~g~ zuzugreifen.', - ['shop_item'] = '%sEUR', - ['bought'] = 'Du kaufst ~b~%sx %s~s~ für ~b~%sEUR', - ['not_enough'] = 'Du hast ~r~nicht~s~ genügend Geld! Dir Fehlt ~b~%sEUR!', - ['player_cannot_hold'] = 'Du hast ~r~nicht~s~ genügend freien Platz in deinem Inventar!', - ['shop_confirm'] = 'Willst du %sx %s kaufen für %sEUR?', - ['no'] = 'Nein', - ['yes'] = 'Ja', - ['amount'] = 'Anzahl', - ['amount_placeholder'] = 'Anzahl die du kaufen möchtest', - ['confirm'] = 'Bestätigen', - ['purchase'] = 'Kaufen', - ['bread'] = 'Brot', - ['water'] = 'Wasser', + ['open_shop_help'] = 'Drücke ~INPUT_CONTEXT~, um den Shop zu öffnen', + ['player_not_found'] = 'Spieler nicht gefunden', + ['cart_empty'] = 'Warenkorb ist leer', + ['invalid_cart_entry'] = 'Ungültiger Warenkorbeintrag', + ['invalid_item_data'] = 'Ungültige Artikeldaten', + ['unknown_item'] = 'Unbekannter Artikel: %s', + ['invalid_quantity'] = 'Ungültige Menge', + ['not_enough_cash'] = 'Nicht genug Bargeld', + ['not_enough_bank'] = 'Unzureichendes Bankguthaben', + ['purchase_success'] = 'Kauf erfolgreich' + } diff --git a/[esx_addons]/esx_shops/locales/en.lua b/[esx_addons]/esx_shops/locales/en.lua index 2b623615..d16324d5 100644 --- a/[esx_addons]/esx_shops/locales/en.lua +++ b/[esx_addons]/esx_shops/locales/en.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['en'] = { ['shop'] = 'shop', - ['shops'] = 'shops', - ['press_menu'] = 'press [%s] to access the ~g~store.', - ['shop_item'] = '$%s', - ['bought'] = 'You Have Bought ~b~%sx %s~s~ for ~b~$%s', - ['not_enough'] = 'you do ~r~not~s~ have enough money, you\'re missing ~b~$%s!', - ['player_cannot_hold'] = 'you do ~r~not~s~ have enough free space in your inventory!', - ['shop_confirm'] = 'buy %sx %s for $%s?', - ['no'] = 'no', - ['yes'] = 'yes', - ['amount'] = 'Amount', - ['amount_placeholder'] = 'Amount you want to buy', - ['confirm'] = 'Confirm', - ['purchase'] = 'Purchase', - ['bread'] = 'Bread', - ['water'] = 'Water', + ['open_shop_help'] = 'Press ~INPUT_CONTEXT~ to open shop', + ['player_not_found'] = 'Player not found', + ['cart_empty'] = 'Cart is empty', + ['invalid_cart_entry'] = 'Invalid cart entry', + ['invalid_item_data'] = 'Invalid item data', + ['unknown_item'] = 'Unknown item: %s', + ['invalid_quantity'] = 'Invalid quantity', + ['not_enough_cash'] = 'Not enough cash', + ['not_enough_bank'] = 'Not enough bank balance', + ['purchase_success'] = 'Purchase successful' + } diff --git a/[esx_addons]/esx_shops/locales/es.lua b/[esx_addons]/esx_shops/locales/es.lua index 90865108..1b11a648 100644 --- a/[esx_addons]/esx_shops/locales/es.lua +++ b/[esx_addons]/esx_shops/locales/es.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['es'] = { ['shop'] = 'tienda', - ['shops'] = 'tiendas', - ['press_menu'] = 'pulsa [%s] para comprar en la tienda.', - ['shop_item'] = '%s€', - ['bought'] = 'has comprado %sx %s por ~r~%s€', - ['not_enough'] = 'no tienes ~r~suficiente dinero: %s', - ['player_cannot_hold'] = 'no tienes espacio libre en tu inventario...', - ['shop_confirm'] = '¿Comprar %sx %s por $%s?', - ['no'] = 'no', - ['yes'] = 'si', - ['amount'] = 'Amount', --not translated - ['amount_placeholder'] = 'Amount you want to buy', --not translated - ['confirm'] = 'Confirm', --not translated - ['purchase'] = 'Purchase', --not translated - ['bread'] = 'Bread', --not translated - ['water'] = 'Water', --not translated + ['open_shop_help'] = 'Pulsa ~INPUT_CONTEXT~ para abrir la tienda', + ['player_not_found'] = 'Jugador no encontrado', + ['cart_empty'] = 'El carrito está vacío', + ['invalid_cart_entry'] = 'Entrada de carrito inválida', + ['invalid_item_data'] = 'Datos de artículo inválidos', + ['unknown_item'] = 'Artículo desconocido: %s', + ['invalid_quantity'] = 'Cantidad inválida', + ['not_enough_cash'] = 'No tienes suficiente efectivo', + ['not_enough_bank'] = 'Saldo bancario insuficiente', + ['purchase_success'] = 'Compra exitosa' + } diff --git a/[esx_addons]/esx_shops/locales/fi.lua b/[esx_addons]/esx_shops/locales/fi.lua index 0db1e83f..71636bdb 100644 --- a/[esx_addons]/esx_shops/locales/fi.lua +++ b/[esx_addons]/esx_shops/locales/fi.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['fi'] = { ['shop'] = 'Kauppa', - ['shops'] = 'Kauppa', - ['press_menu'] = 'Paina [%s] avataksesi valikko.', - ['shop_item'] = '€%s', - ['bought'] = 'Sinä ostit juuri %sx %s. Summaksi tuli ~r~€%s', - ['not_enough'] = 'Sinulla ei ole ~r~tarpeeksi rahaa, sinulta puuttuu ~r~€%s!', - ['player_cannot_hold'] = 'Sinulla ~r~ei ole tarpeeksi tilaa repussasi!', - ['shop_confirm'] = 'Osta %sx %s hintaan €%s?', - ['no'] = 'Ei', - ['yes'] = 'Kyllä', - ['amount'] = 'Määrä', - ['amount_placeholder'] = 'Kuinka monta haluat ostaa?', - ['confirm'] = 'Vahvista', - ['purchase'] = 'Osta', - ['bread'] = 'Leipä', - ['water'] = 'Vesi', + ['open_shop_help'] = 'Paina ~INPUT_CONTEXT~ avataksesi kaupan', + ['player_not_found'] = 'Pelaajaa ei löydy', + ['cart_empty'] = 'Ostoskori on tyhjä', + ['invalid_cart_entry'] = 'Virheellinen ostoskori-merkintä', + ['invalid_item_data'] = 'Virheelliset tuotetiedot', + ['unknown_item'] = 'Tuntematon tuote: %s', + ['invalid_quantity'] = 'Virheellinen määrä', + ['not_enough_cash'] = 'Ei tarpeeksi käteistä', + ['not_enough_bank'] = 'Riittämätön pankkisaldo', + ['purchase_success'] = 'Osto onnistui' + } diff --git a/[esx_addons]/esx_shops/locales/fr.lua b/[esx_addons]/esx_shops/locales/fr.lua index 088998dc..f47280e0 100644 --- a/[esx_addons]/esx_shops/locales/fr.lua +++ b/[esx_addons]/esx_shops/locales/fr.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['fr'] = { ['shop'] = 'magasin', - ['shops'] = 'magasins', - ['press_menu'] = 'appuyez sur [%s] pour accéder au magasin.', - ['shop_item'] = '$%s', - ['bought'] = 'vous venez d\'acheter %sx %s pour ~r~$%s', - ['not_enough'] = 'vous n\'avez ~r~pas assez d\'argent: %s', - ['player_cannot_hold'] = 'vous n\'avez ~r~pas assez de place dans votre inventaire!', - ['shop_confirm'] = 'acheter %sx %s pour $%s?', - ['no'] = 'non', - ['yes'] = 'oui', - ['amount'] = 'Quantité', - ['amount_placeholder'] = 'Quantité que vous voulez acheter', - ['confirm'] = 'Confirmer', - ['purchase'] = 'Acheter', - ['bread'] = 'Pain', - ['water'] = 'Eau', + ['open_shop_help'] = 'Appuyez sur ~INPUT_CONTEXT~ pour ouvrir le magasin', + ['player_not_found'] = 'Joueur introuvable', + ['cart_empty'] = 'Le panier est vide', + ['invalid_cart_entry'] = 'Entrée de panier invalide', + ['invalid_item_data'] = 'Données d\'objet invalides', + ['unknown_item'] = 'Objet inconnu : %s', + ['invalid_quantity'] = 'Quantité invalide', + ['not_enough_cash'] = 'Pas assez d\'argent liquide', + ['not_enough_bank'] = 'Solde bancaire insuffisant', + ['purchase_success'] = 'Achat réussi' + } diff --git a/[esx_addons]/esx_shops/locales/hu.lua b/[esx_addons]/esx_shops/locales/hu.lua index 949a0838..679da344 100644 --- a/[esx_addons]/esx_shops/locales/hu.lua +++ b/[esx_addons]/esx_shops/locales/hu.lua @@ -1,18 +1,16 @@ +---@diagnostic disable: undefined-global Locales['hu'] = { ['shop'] = 'Bolt', - ['shops'] = 'Bolt', - ['press_menu'] = 'Nyomd meg a [%s] gombot hogy megnézd a kinálatot', - ['shop_item'] = '$%s', - ['bought'] = 'Vettél %sx %s ennyiért: ~r~$%s', - ['not_enough'] = 'Nincsen elég pénzed', - ['player_cannot_hold'] = 'Nincsen elég szabad helyed!', - ['shop_confirm'] = 'Veszel %sx %s ennyiért $%s?', - ['no'] = 'Nem', - ['yes'] = 'Igen', - ['amount'] = 'Mennyiség', - ['amount_placeholder'] = 'Amennyit szeretnél', - ['confirm'] = 'Megerősítés', - ['purchase'] = 'Vásárlás', - ['bread'] = 'Kenyér', - ['water'] = 'Palackos víz', + + ['open_shop_help'] = 'Nyomd meg a ~INPUT_CONTEXT~ gombot a bolt megnyitásához', + ['player_not_found'] = 'Játékos nem található', + ['cart_empty'] = 'A kosár üres', + ['invalid_cart_entry'] = 'Érvénytelen kosár bejegyzés', + ['invalid_item_data'] = 'Érvénytelen tárgy adatok', + ['unknown_item'] = 'Ismeretlen tárgy: %s', + ['invalid_quantity'] = 'Érvénytelen mennyiség', + ['not_enough_cash'] = 'Nincs elég készpénz', + ['not_enough_bank'] = 'Elégtelen bankszámla egyenleg', + ['purchase_success'] = 'Sikeres vásárlás' + } diff --git a/[esx_addons]/esx_shops/locales/it.lua b/[esx_addons]/esx_shops/locales/it.lua index d88bac6a..e8811a7a 100644 --- a/[esx_addons]/esx_shops/locales/it.lua +++ b/[esx_addons]/esx_shops/locales/it.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['it'] = { ['shop'] = 'negozio', - ['shops'] = 'negozi', - ['press_menu'] = 'premi [%s] per accedere al negozio.', - ['shop_item'] = '%s$', - ['bought'] = 'hai acquistato %sx %s per ~r~%s$', - ['not_enough'] = 'non hai ~r~abbastanza soldi: %s', - ['player_cannot_hold'] = 'non hai spazio libero nel tuo inventario', - ['shop_confirm'] = 'Acquista %sx %s per $%s?', - ['no'] = 'no', - ['yes'] = 'sì', - ['amount'] = 'Quantità', - ['amount_placeholder'] = 'Quantità che desideri acquistare', - ['confirm'] = 'Conferma', - ['purchase'] = 'Acquista', - ['bread'] = 'Pane', - ['water'] = 'Acqua', + ['open_shop_help'] = 'Premi ~INPUT_CONTEXT~ per aprire il negozio', + ['player_not_found'] = 'Giocatore non trovato', + ['cart_empty'] = 'Il carrello è vuoto', + ['invalid_cart_entry'] = 'Voce del carrello non valida', + ['invalid_item_data'] = 'Dati articolo non validi', + ['unknown_item'] = 'Articolo sconosciuto: %s', + ['invalid_quantity'] = 'Quantità non valida', + ['not_enough_cash'] = 'Contanti insufficienti', + ['not_enough_bank'] = 'Saldo bancario insufficiente', + ['purchase_success'] = 'Acquisto avvenuto con successo' + } diff --git a/[esx_addons]/esx_shops/locales/nl.lua b/[esx_addons]/esx_shops/locales/nl.lua index 81eaa172..16cc4b96 100644 --- a/[esx_addons]/esx_shops/locales/nl.lua +++ b/[esx_addons]/esx_shops/locales/nl.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['nl'] = { ['shop'] = 'winkel', - ['shops'] = 'winkels', - ['press_menu'] = 'klik op [%s] om de ~g~winkel~s~ te gebruiken.', - ['shop_item'] = '€%s', - ['bought'] = 'Je hebt ~b~%sx %s~s~ gekocht voor ~b~€%s', - ['not_enough'] = 'je hebt ~r~niet~s~ genoeg geld, je mist nog ~b~€%s!', - ['player_cannot_hold'] = 'je hebt ~r~niet~s~ genoeg ruimte in je inventaris!', - ['shop_confirm'] = 'Wil je %sx %s kopen voor €%s?', - ['no'] = 'nee', - ['yes'] = 'ja', - ['amount'] = 'Bedrag', - ['amount_placeholder'] = 'Hoeveelheid dat je wil kopen', - ['confirm'] = 'Bevestig', - ['purchase'] = 'Koop', - ['bread'] = 'Brood', - ['water'] = 'Water', + ['open_shop_help'] = 'Druk op ~INPUT_CONTEXT~ om de winkel te openen', + ['player_not_found'] = 'Speler niet gevonden', + ['cart_empty'] = 'Winkelwagen is leeg', + ['invalid_cart_entry'] = 'Ongeldige winkelwageninvoer', + ['invalid_item_data'] = 'Ongeldige artikelgegevens', + ['unknown_item'] = 'Onbekend item: %s', + ['invalid_quantity'] = 'Ongeldige hoeveelheid', + ['not_enough_cash'] = 'Niet genoeg contant geld', + ['not_enough_bank'] = 'Onvoldoende banksaldo', + ['purchase_success'] = 'Aankoop geslaagd' + } diff --git a/[esx_addons]/esx_shops/locales/pl.lua b/[esx_addons]/esx_shops/locales/pl.lua index b6a5abce..382e1070 100644 --- a/[esx_addons]/esx_shops/locales/pl.lua +++ b/[esx_addons]/esx_shops/locales/pl.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['pl'] = { ['shop'] = 'sklep', - ['shops'] = 'sklepy', - ['press_menu'] = 'naciśnij [%s] żeby wejść do sklepu.', - ['shop_item'] = '$%s', - ['bought'] = 'właśnie zakupiłeś %s x %s za %s $', - ['not_enough'] = 'nie masz ~r~wystarczjąco pięniędzy, Brakuje Ci ~r~$%s!', - ['player_cannot_hold'] = '~r~Nie masz wystarczająco wolnego miejsca w swoim ekwipunku!', - ['shop_confirm'] = 'chcesz kupić %sx %s za $%s?', - ['no'] = 'nie', - ['yes'] = 'tak', - ['amount'] = 'Amount', --not translated - ['amount_placeholder'] = 'Amount you want to buy', --not translated - ['confirm'] = 'Confirm', --not translated - ['purchase'] = 'Purchase', --not translated - ['bread'] = 'Bread', --not translated - ['water'] = 'Water', --not translated + ['open_shop_help'] = 'Naciśnij ~INPUT_CONTEXT~, aby otworzyć sklep', + ['player_not_found'] = 'Nie znaleziono gracza', + ['cart_empty'] = 'Koszyk jest pusty', + ['invalid_cart_entry'] = 'Nieprawidłowa pozycja koszyka', + ['invalid_item_data'] = 'Nieprawidłowe dane przedmiotu', + ['unknown_item'] = 'Nieznany przedmiot: %s', + ['invalid_quantity'] = 'Nieprawidłowa ilość', + ['not_enough_cash'] = 'Za mało gotówki', + ['not_enough_bank'] = 'Niewystarczające saldo bankowe', + ['purchase_success'] = 'Zakup udany' + } diff --git a/[esx_addons]/esx_shops/locales/sl.lua b/[esx_addons]/esx_shops/locales/sl.lua index 8e05d6ea..990f19ed 100644 --- a/[esx_addons]/esx_shops/locales/sl.lua +++ b/[esx_addons]/esx_shops/locales/sl.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['sl'] = { ['shop'] = 'Trgovina', - ['shops'] = 'Trgovine', - ['press_menu'] = 'pritisni [%s] da odpres ~g~Trgovino.', - ['shop_item'] = '$%s', - ['bought'] = 'Vi ste kupili ~b~%sx %s~s~ za ~b~$%s', - ['not_enough'] = 'Vi ~r~nimate~s~ dovolj denarja, manjka vam ~b~$%s!', - ['player_cannot_hold'] = 'Vi ~r~nimate~s~ dovolj prostora v vasi shrambi!', - ['shop_confirm'] = 'kupi %sx %s za $%s?', - ['no'] = 'ne', - ['yes'] = 'da', - ['amount'] = 'vsota', - ['amount_placeholder'] = 'Koliko kosov bi kupili?', - ['confirm'] = 'Potrdi', - ['purchase'] = 'Kupi', - ['bread'] = 'Krh', - ['water'] = 'Voda', + ['open_shop_help'] = 'Pritisnite ~INPUT_CONTEXT~, da odprete trgovino', + ['player_not_found'] = 'Igralec ni najden', + ['cart_empty'] = 'Košarica je prazna', + ['invalid_cart_entry'] = 'Neveljaven vnos v košarici', + ['invalid_item_data'] = 'Neveljavni podatki o izdelku', + ['unknown_item'] = 'Neznan izdelek: %s', + ['invalid_quantity'] = 'Neveljavna količina', + ['not_enough_cash'] = 'Premalo gotovine', + ['not_enough_bank'] = 'Premalo sredstev na računu', + ['purchase_success'] = 'Nakup uspešen' + } diff --git a/[esx_addons]/esx_shops/locales/sr.lua b/[esx_addons]/esx_shops/locales/sr.lua index 2831dd29..dc3c1b91 100644 --- a/[esx_addons]/esx_shops/locales/sr.lua +++ b/[esx_addons]/esx_shops/locales/sr.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['sr'] = { ['shop'] = 'Prodavnica', - ['shops'] = 'Prodavnice', - ['press_menu'] = 'Pritisni [%s] da pristupiš ~g~prodavnici.', - ['shop_item'] = '$%s', - ['bought'] = 'Kupili ste ~b~%sx %s~s~ za ~b~$%s', - ['not_enough'] = 'Vi ~r~nemate~s~ dovoljno novca, nedostaje vam ~b~$%s!', - ['player_cannot_hold'] = 'Vi ~r~nemate~s~ dovoljno mesta u vašem inventaru!', - ['shop_confirm'] = 'Kupi %sx %s za $%s?', - ['no'] = 'Ne', - ['yes'] = 'Da', - ['amount'] = 'Amount', --not translated - ['amount_placeholder'] = 'Amount you want to buy', --not translated - ['confirm'] = 'Confirm', --not translated - ['purchase'] = 'Purchase', --not translated - ['bread'] = 'Bread', --not translated - ['water'] = 'Water', --not translated + ['open_shop_help'] = 'Pritisnite ~INPUT_CONTEXT~ da otvorite prodavnicu', + ['player_not_found'] = 'Igrač nije pronađen', + ['cart_empty'] = 'Korpa je prazna', + ['invalid_cart_entry'] = 'Nevažeći unos u korpi', + ['invalid_item_data'] = 'Nevažeći podaci o artiklu', + ['unknown_item'] = 'Nepoznat artikal: %s', + ['invalid_quantity'] = 'Nevažeća količina', + ['not_enough_cash'] = 'Nedovoljno gotovine', + ['not_enough_bank'] = 'Nedovoljno sredstava u banci', + ['purchase_success'] = 'Kupovina uspešna' + } diff --git a/[esx_addons]/esx_shops/locales/sv.lua b/[esx_addons]/esx_shops/locales/sv.lua index 0d419f6e..26b91ff2 100644 --- a/[esx_addons]/esx_shops/locales/sv.lua +++ b/[esx_addons]/esx_shops/locales/sv.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['sv'] = { - ['shop'] = 'Affär', - ['shops'] = 'Affärer', - ['press_menu'] = 'Tryck [%s] för att öppna ~g~affären.', - ['shop_item'] = '%skr', - ['bought'] = 'Du har köpt ~b~%sx %s~s~ för ~b~%skr', - ['not_enough'] = 'Du har ~r~inte~s~ råd, det fattas ~b~%skr!', - ['player_cannot_hold'] = 'Du har ~r~inte~s~ plats i inventoryt för detta!', - ['shop_confirm'] = 'Köp %sx %s för %skr?', - ['no'] = 'Ja', - ['yes'] = 'Nej', - ['amount'] = 'Antal', - ['amount_placeholder'] = 'Antal du vill köpa', - ['confirm'] = 'Godkänn', - ['purchase'] = 'Köp', - ['bread'] = 'Bröd', - ['water'] = 'Vatten', - } + ['shop'] = 'Affär', + ['open_shop_help'] = 'Tryck ~INPUT_CONTEXT~ för att öppna affären', + ['player_not_found'] = 'Spelare hittades inte', + ['cart_empty'] = 'Kundvagnen är tom', + ['invalid_cart_entry'] = 'Ogiltig kundvagnspost', + ['invalid_item_data'] = 'Ogiltig artikeldata', + ['unknown_item'] = 'Okänt objekt: %s', + ['invalid_quantity'] = 'Ogiltig mängd', + ['not_enough_cash'] = 'Inte tillräckligt med kontanter', + ['not_enough_bank'] = 'Otillräckligt banksaldo', + ['purchase_success'] = 'Köpet lyckades' + +} diff --git a/[esx_addons]/esx_shops/locales/tr.lua b/[esx_addons]/esx_shops/locales/tr.lua index 78315a8d..bebf87bd 100644 --- a/[esx_addons]/esx_shops/locales/tr.lua +++ b/[esx_addons]/esx_shops/locales/tr.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['tr'] = { ['shop'] = 'Market', - ['shops'] = 'Marketler', - ['press_menu'] = 'Marketi açmak için ~b~[%s]~s~ tuşuna bas.', - ['shop_item'] = '$%s', - ['bought'] = '~b~%s~s~x ~b~%s~s~ satın aldın ve ~b~$%s ~s~ödedin.', - ['not_enough'] = 'Paranız yeterli ~r~değil~s~, ~b~$%s ~s~eksik!', - ['player_cannot_hold'] = 'Envanterinizde boş yer ~r~yok~s~!', - ['shop_confirm'] = '%sx %s için $%s satın almak istiyor musun?', - ['no'] = 'hayır', - ['yes'] = 'evet', - ['amount'] = 'Miktar', - ['amount_placeholder'] = 'Satın almak istediğin miktar', - ['confirm'] = 'Onayla', - ['purchase'] = 'Satın Al', - ['bread'] = 'Ekmek', - ['water'] = 'Su', + ['open_shop_help'] = 'Mağazayı açmak için ~INPUT_CONTEXT~ tuşuna basın', + ['player_not_found'] = 'Oyuncu bulunamadı', + ['cart_empty'] = 'Sepet boş', + ['invalid_cart_entry'] = 'Geçersiz sepet girdisi', + ['invalid_item_data'] = 'Geçersiz ürün verisi', + ['unknown_item'] = 'Bilinmeyen ürün: %s', + ['invalid_quantity'] = 'Geçersiz miktar', + ['not_enough_cash'] = 'Yeterli nakit yok', + ['not_enough_bank'] = 'Yetersiz banka bakiyesi', + ['purchase_success'] = 'Satın alma başarılı' + } diff --git a/[esx_addons]/esx_shops/locales/zh-cn.lua b/[esx_addons]/esx_shops/locales/zh-cn.lua index ac7e27cf..588218f9 100644 --- a/[esx_addons]/esx_shops/locales/zh-cn.lua +++ b/[esx_addons]/esx_shops/locales/zh-cn.lua @@ -1,18 +1,15 @@ +---@diagnostic disable: undefined-global Locales['zh-cn'] = { ['shop'] = '购物商店', - ['shops'] = '购物商店', - ['press_menu'] = '键下 [%s] 访问~g~购物商店.', - ['shop_item'] = '$%s', - ['bought'] = '已购 ~b~%sx %s~s~ -支付:~b~$%s', - ['not_enough'] = '暂无足够资金, 您还需要~b~$%s!', - ['player_cannot_hold'] = '背包尚无足够剩余空间!', - ['shop_confirm'] = '确认购买 %sX%s -支付:$%s?', - ['no'] = '取消', - ['yes'] = '确认', - ['amount'] = 'Amount', --not translated - ['amount_placeholder'] = 'Amount you want to buy', --not translated - ['confirm'] = 'Confirm', --not translated - ['purchase'] = 'Purchase', --not translated - ['bread'] = 'Bread', --not translated - ['water'] = 'Water', --not translated + ['open_shop_help'] = '按下 ~INPUT_CONTEXT~ 打开商店', + ['player_not_found'] = '未找到玩家', + ['cart_empty'] = '购物车为空', + ['invalid_cart_entry'] = '购物车条目无效', + ['invalid_item_data'] = '物品数据无效', + ['unknown_item'] = '未知物品:%s', + ['invalid_quantity'] = '数量无效', + ['not_enough_cash'] = '现金不足', + ['not_enough_bank'] = '银行余额不足', + ['purchase_success'] = '购买成功' + } diff --git a/[esx_addons]/esx_shops/server/main.lua b/[esx_addons]/esx_shops/server/main.lua index 895bf0ae..86a7c633 100644 --- a/[esx_addons]/esx_shops/server/main.lua +++ b/[esx_addons]/esx_shops/server/main.lua @@ -19,7 +19,7 @@ end RegisterServerEvent('esx_shops:buyItem') AddEventHandler('esx_shops:buyItem', function(itemName, amount, zone) local source = source - local xPlayer = ESX.Player(source) + local xPlayer = ESX.GetPlayerFromId(source) local Exists, price, label = GetItemFromShop(itemName, zone) amount = ESX.Math.Round(amount) @@ -28,26 +28,58 @@ AddEventHandler('esx_shops:buyItem', function(itemName, amount, zone) return end - if not Exists then - print(('[^3WARNING^7] Player ^5%s^7 attempted to exploit the shop!'):format(source)) - return - end + local method = data and data.method or 'cash' + local cart = data and data.cart or {} + if type(cart) ~= 'table' or #cart == 0 then + cb(false, _U('cart_empty')) + return + end - if Exists then - price = price * amount - -- can the player afford this item? - if xPlayer.getMoney() >= price then - -- can the player carry the said amount of x item? - if xPlayer.canCarryItem(itemName, amount) then - xPlayer.removeMoney(price, label .. " " .. TranslateCap('purchase')) - xPlayer.addInventoryItem(itemName, amount) - xPlayer.showNotification(TranslateCap('bought', amount, label, ESX.Math.GroupDigits(price))) - else - xPlayer.showNotification(TranslateCap('player_cannot_hold')) - end - else - local missingMoney = price - xPlayer.getMoney() - xPlayer.showNotification(TranslateCap('not_enough', ESX.Math.GroupDigits(missingMoney))) - end - end + local maxQty = Config.ItemMaxQuantity or 100 + + local total = 0 + local normalizedCart = {} + for _, entry in ipairs(cart) do + if type(entry) ~= 'table' then + cb(false, _U('invalid_cart_entry')) + return + end + local name = entry.name + local amount = tonumber(entry.amount) + if not name or not amount then + cb(false, _U('invalid_item_data')) + return + end + local def = itemIndex[name] + if not def then + cb(false, _U('unknown_item', name)) + return + end + if amount < 1 or amount > maxQty then + cb(false, _U('invalid_quantity')) + return + end + total = total + (def.price * amount) + normalizedCart[#normalizedCart + 1] = { name = name, amount = amount } + end + + if method == 'bank' then + if xPlayer.getAccount('bank').money < total then + cb(false, _U('not_enough_bank')) + return + end + xPlayer.removeAccountMoney('bank', total) + else + if xPlayer.getMoney() < total then + cb(false, _U('not_enough_cash')) + return + end + xPlayer.removeMoney(total) + end + + for _, entry in ipairs(normalizedCart) do + xPlayer.addInventoryItem(entry.name, entry.amount) + end + + cb(true, _U('purchase_success')) end) From 6f81ff4bdb92e267a4aaaf7ad13cc7301978b112 Mon Sep 17 00:00:00 2001 From: SyncOusli <142453413+SyncOusli@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:26:47 +0100 Subject: [PATCH 2/2] Add HTML and CSS for esx_shops interface - Created index.html for the shop layout including header, categories, items, and shopping cart. - Implemented style.css for responsive design and aesthetics, utilizing Flexbox and Grid for layout. - Integrated Font Awesome for icons and Google Fonts for typography. - Added placeholder items with prices and images for demonstration purposes. --- [esx_addons]/esx_shops/client/main.lua | 10 - [esx_addons]/esx_shops/config.lua | 2 +- [esx_addons]/esx_shops/fxmanifest.lua | 2 +- [esx_addons]/esx_shops/html/app.js | 270 ++++++++++ [esx_addons]/esx_shops/html/index.html | 668 +++++++++++++++++++++++++ [esx_addons]/esx_shops/html/style.css | 506 +++++++++++++++++++ [esx_addons]/esx_shops/server/main.lua | 57 ++- 7 files changed, 1479 insertions(+), 36 deletions(-) create mode 100644 [esx_addons]/esx_shops/html/app.js create mode 100644 [esx_addons]/esx_shops/html/index.html create mode 100644 [esx_addons]/esx_shops/html/style.css diff --git a/[esx_addons]/esx_shops/client/main.lua b/[esx_addons]/esx_shops/client/main.lua index 3f1aa5fb..d0b8f6f2 100644 --- a/[esx_addons]/esx_shops/client/main.lua +++ b/[esx_addons]/esx_shops/client/main.lua @@ -18,16 +18,6 @@ local function closeShopUi() setShopUiVisible(false) end -RegisterCommand('shopui', function() - if isShopUiOpen then - closeShopUi() - else - openShopUi() - end -end, false) - -RegisterKeyMapping('shopui', 'Toggle Shop UI', 'keyboard', 'F7') - RegisterNUICallback('close', function(_, cb) closeShopUi() if cb then cb({}) end diff --git a/[esx_addons]/esx_shops/config.lua b/[esx_addons]/esx_shops/config.lua index 39d54b68..4bd28e17 100644 --- a/[esx_addons]/esx_shops/config.lua +++ b/[esx_addons]/esx_shops/config.lua @@ -1,6 +1,6 @@ Config = {} -Config.Locale = GetConvar('esx:locale', 'fr') +Config.Locale = GetConvar('esx:locale', 'en') Config.ItemMaxQuantity = 100 diff --git a/[esx_addons]/esx_shops/fxmanifest.lua b/[esx_addons]/esx_shops/fxmanifest.lua index 30387906..e19eabfa 100644 --- a/[esx_addons]/esx_shops/fxmanifest.lua +++ b/[esx_addons]/esx_shops/fxmanifest.lua @@ -33,4 +33,4 @@ files { 'html/index.html', 'html/app.js', 'html/style.css', -} +} \ No newline at end of file diff --git a/[esx_addons]/esx_shops/html/app.js b/[esx_addons]/esx_shops/html/app.js new file mode 100644 index 00000000..5db5ca33 --- /dev/null +++ b/[esx_addons]/esx_shops/html/app.js @@ -0,0 +1,270 @@ +$(function () { + const resourceName = + typeof GetParentResourceName === "function" + ? GetParentResourceName() + : "esx_shops"; + + function nuiPost(action, payload) { + $.post(`https://${resourceName}/${action}`, JSON.stringify(payload || {})); + } + + $(".Container").hide(); + + // State + let categories = []; + let items = []; + let activeCategoryId = null; + let searchTerm = ""; + const cartByName = {}; // name -> { def, amount } + + const $categories = $(".Categories-Scroller"); + const $items = $(".Shop-Items"); + const $basket = $(".Right-Shop-Basket"); + const $basketEmpty = $(".Right-Shop-Basket .Basket-Empty"); + const $totalPrice = $(".Right-Shop-End-Price span").last(); + + function formatPrice(value) { + return `${value}$`; + } + + function getFilteredItems() { + return items.filter((i) => { + const matchesCat = !activeCategoryId || i.category === activeCategoryId; + const matchesSearch = + !searchTerm || (i.label || i.name).toLowerCase().includes(searchTerm); + return matchesCat && matchesSearch; + }); + } + + function renderCategories() { + $categories.empty(); + categories.forEach((c, idx) => { + const $el = $(`
${c.label}
`); + if (!activeCategoryId && idx === 0) activeCategoryId = c.id; + if (c.id === activeCategoryId) $el.addClass("active"); + $el.on("click", () => { + activeCategoryId = c.id; + renderCategories(); + renderItems(); + }); + $categories.append($el); + }); + } + + function createItemCard(item) { + const $card = $(` +
+
+
+
+
+
+ +
+
+
+
Add to Cart
+
+
+ `); + $card.find(".Item-Label").text(item.label || item.name); + $card.find(".Item-Price").text(`$ ${item.price}`); + $card.find("img").attr("src", item.image || ""); + $card + .find(".Item-Cart") + .on("click", () => addToCart(item, item.amount || 1)); + return $card; + } + + function renderItems() { + $items.empty(); + const list = getFilteredItems(); + list.forEach((item) => { + $items.append(createItemCard(item)); + }); + } + + function recomputeTotal() { + let total = 0; + Object.values(cartByName).forEach((entry) => { + total += entry.def.price * entry.amount; + }); + $totalPrice.text(formatPrice(total)); + } + + function renderBasket() { + $basket.find(".Basket-Item").remove(); + const entries = Object.values(cartByName); + if (entries.length === 0) { + $basketEmpty.show(); + } else { + $basketEmpty.hide(); + } + entries.forEach((entry) => { + const { def, amount } = entry; + const $row = $(` +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ `); + $row.find("img").attr("src", def.image || ""); + $row.find(".Basket-Item-Label").text(def.label || def.name); + $row.find(".Basket-Item-Price").text(`${def.price} $`); + const $count = $row.find("input.count"); + $count.val(amount); + $row + .find("button.decrease") + .on("click", () => + updateCart(def.name, cartByName[def.name].amount - 1) + ); + $row + .find("button.increase") + .on("click", () => + updateCart(def.name, cartByName[def.name].amount + 1) + ); + $count.on("change", () => + updateCart(def.name, parseInt($count.val(), 10) || 1) + ); + $row.find(".Count-Remove").on("click", () => removeFromCart(def.name)); + $basket.append($row); + }); + recomputeTotal(); + } + + function addToCart(def, addAmount) { + const maxQty = 100; + const name = def.name; + if (!cartByName[name]) { + cartByName[name] = { def, amount: 0 }; + } + const prevAmount = cartByName[name].amount; + cartByName[name].amount = Math.min( + maxQty, + cartByName[name].amount + (addAmount || 1) + ); + const addedNow = cartByName[name].amount - prevAmount; + if (addedNow > 0) { + console.log( + `[Shop] Added to basket: ${ + def.label || def.name + } x${addedNow} (total: ${cartByName[name].amount})` + ); + } else { + console.warn( + `[Shop] Not added: ${def.label || def.name} (reached max ${maxQty})` + ); + } + renderBasket(); + } + + function updateCart(name, newAmount) { + if (!cartByName[name]) return; + if (newAmount <= 0) { + delete cartByName[name]; + } else { + const maxQty = 100; + cartByName[name].amount = Math.min(maxQty, newAmount); + } + renderBasket(); + } + + function removeFromCart(name) { + if (cartByName[name]) { + delete cartByName[name]; + renderBasket(); + } + } + + function getCartPayload() { + const cart = []; + Object.values(cartByName).forEach((entry) => { + cart.push({ name: entry.def.name, amount: entry.amount }); + }); + return cart; + } + + function checkout(method) { + const cart = getCartPayload(); + if (cart.length === 0) return; + $.post( + `https://${resourceName}/purchase`, + JSON.stringify({ method, cart }), + function (resp) { + try { + const data = typeof resp === "string" ? JSON.parse(resp) : resp; + if (data && data.success) { + Object.keys(cartByName).forEach((k) => delete cartByName[k]); + renderBasket(); + } else { + } + } catch (_) {} + } + ); + } + + window.addEventListener("message", function (event) { + const data = event.data || {}; + switch (data.action) { + case "show": + $(".Container").stop(true, true).fadeIn(150); + $.post( + `https://${resourceName}/getShopData`, + JSON.stringify({}), + function (resp) { + try { + const payload = + typeof resp === "string" ? JSON.parse(resp) : resp; + categories = payload.categories || []; + items = payload.items || []; + activeCategoryId = (categories[0] && categories[0].id) || null; + renderCategories(); + renderItems(); + renderBasket(); + } catch (_) {} + } + ); + break; + case "hide": + $(".Container").stop(true, true).fadeOut(150); + break; + } + }); + + $(".Shop-close").on("click", function () { + nuiPost("close"); + }); + + $(document).on("keydown", function (e) { + if (e.key === "Escape") { + nuiPost("close"); + } + }); + + $(".Shop-search input[type=text]").on("input", function () { + searchTerm = ($(this).val() || "").toString().toLowerCase(); + renderItems(); + }); + + $(".Right-Shop-End-Button .Cash").on("click", function () { + checkout("cash"); + }); + $(".Right-Shop-End-Button .Bank").on("click", function () { + checkout("bank"); + }); +}); diff --git a/[esx_addons]/esx_shops/html/index.html b/[esx_addons]/esx_shops/html/index.html new file mode 100644 index 00000000..553778cd --- /dev/null +++ b/[esx_addons]/esx_shops/html/index.html @@ -0,0 +1,668 @@ + + + + + + + esx_shops + + + + + + +
+
+
+
+ +
+
+ FIVEM 24/7 SHOP +
+
+ +
+
+
+
+
+
Drinks
+
Food
+
Essentials
+
Others
+
Others
+
Others
+
Others Pendejo
+
+
+
+
+
+
Donut
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Lockpick
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Sprunk
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Pizza
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Water
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Donut
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Lockpick
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Sprunk
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Pizza
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Water
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Donut
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Lockpick
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Sprunk
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Pizza
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Water
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Donut
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Lockpick
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Sprunk
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Pizza
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
Water
+
$ 999999
+
+
+ +
+
+
+ +
+
+ Add to Cart +
+
+
+
+
+
+
+ + SHOPPING CART +
+
+
+ + Your basket is empty + Add something to checkout +
+
+
+
+ +
+
+
Donut
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Sprunk
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Donut
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Water
+
200 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Pizza
+
9900 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Donut
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Donut
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ + +
+
+
+
+
+
+ +
+
+
Sprunk
+
999999 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Water
+
200 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
Pizza
+
9900 $
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ TOTAL PRICE: + 5000$ +
+
+
+
+ +
+
+ CASH +
+
+
+
+ +
+
+ BANK +
+
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/[esx_addons]/esx_shops/html/style.css b/[esx_addons]/esx_shops/html/style.css new file mode 100644 index 00000000..95f04b5c --- /dev/null +++ b/[esx_addons]/esx_shops/html/style.css @@ -0,0 +1,506 @@ +@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); + +* { + margin: 0; + padding: 0; +} + +.Container { + background: rgba(22, 22, 22, 1); + width: 80vw; + height: 80vh; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: none; +} + +.Header-Shop { + display: flex; + width: 100%; + /* background-color: red; */ + justify-content: space-between; + height: 5vh; + margin-top: 0.5vw; +} + +.Shop-name { + display: flex; + align-items: center; + font-family: "Poppins", sans-serif; + margin-left: 1vw; + gap: 0.5vw; +} + +.Shop-search { + display: flex; + align-items: center; + margin-right: 1vw; + gap: 0.5vw; +} + +.Shop-title { + color: rgba(242, 242, 242, 1); + font-family: "Poppins", sans-serif; + font-weight: 600; + font-size: 1vw; +} + +.Shop-icon { + color: rgba(242, 242, 242, 1); + font-size: 1vw; +} + +.Shop-search-icon input { + border: none; + /* remove style */ + background: rgba(242, 242, 242, 0.1); + color: rgba(242, 242, 242, 1); + font-family: "Poppins", sans-serif; + font-weight: 400; + font-size: 0.8vw; + border-radius: 0.2vw; + padding: 0 0.5vw; + height: 3.4vh; + width: 50vw; + padding-left: 2vw; +} + +.Shop-search-icon input:focus { + outline: none; +} + +.Shop-search-icon { + position: relative; +} + +.Shop-search-icon i { + position: absolute; + left: 0.5vw; + top: 50%; + transform: translateY(-50%); + color: #aaa; + pointer-events: none; + font-size: 0.8vw; +} + +.Shop-close { + background-color: rgba(242, 242, 242, 0.1); + color: rgba(242, 242, 242, 1); + height: 3.4vh; + width: 2vw; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.2vw; +} + +.Shop-close i { + font-size: 1vw; +} + +.Container-Shop { + margin-top: 0.5vw; + display: flex; + width: 100%; + /* background-color: red; */ +} + +.Left-Shop { + width: 70%; + /* background-color: blue; */ + height: 2vw; +} + +.Right-Shop { + width: 28%; + /* background-color: green; */ + height: 10vw; +} + +.Right-Shop-Title { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5vw; + /* background-color: red; */ +} + +.Right-Shop-Basket { + margin-top: 1.5vw; + /* background-color: red; */ + height: 55vh; + width: 100%; + overflow-y: auto; + padding-right: 0.5vw; + display: flex; + flex-direction: column; + position: relative; +} + +.Right-Shop-Title span { + color: white; + font-family: "Poppins", sans-serif; + font-weight: 600; + font-size: 0.9vw; +} + +.Right-Shop-Title i { + color: rgba(250, 250, 250, 1); + font-size: 0.8vw; +} + +.Basket-Item { + background-color: #212121; + display: flex; + align-items: center; + margin-bottom: 0.5vw; + border-radius: 0.2vw; + gap: 0.5vw; + color: white; + justify-content: space-between; +} + +.Basket-Item-LeftSection, +.Basket-Item-RightSection { + display: flex; + align-items: center; + justify-content: center; +} + +.Basket-Item-Info { + color: white; + font-family: "Poppins", sans-serif; + font-weight: 500; +} + +.Basket-Item-Label { + font-size: 0.6vw; +} + +.Basket-Item-Price { + font-size: 0.7vw; +} + +.Basket-Item-Img img { + width: 2vw; + height: 2vw; + border-radius: 0.2vw; + padding: 0.3vw; +} + +.Shop-Categories { + margin-left: 1vw; + overflow-x: auto; + /* Important for scroll */ + max-width: calc(15% * 6 + 0.5vw * 5); + /* Space for 6 items + 5 gaps */ + scrollbar-width: none; +} + +.Shop-Categories::-webkit-scrollbar { + display: none; + /* Chrome, Safari, Opera */ +} + +.Categories-Scroller { + display: flex; + gap: 0.5vw; + min-width: fit-content; + /* Make sure content does not shrink */ +} + +.Categories-Wrap { + background: rgba(242, 242, 242, 0.1); + color: rgba(242, 242, 242, 0.5); + font-family: Poppins; + font-weight: 500; + font-size: 0.8vw; + padding: 0.3vw 0.5vw; + width: 14%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.3vw; + white-space: nowrap; + flex-shrink: 0; + /* Prevent shrinking in scroll */ +} + +.Categories-Wrap.active { + background: rgba(251, 155, 4, 1); + color: rgba(22, 22, 22, 1); +} + +.Shop-Items { + margin-top: 1vw; + margin-left: 1vw; + width: 94.5%; + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5vw; + max-height: 64vh; + /* Limit height for scrolling */ + /* background-color: red; */ + overflow-y: auto; + /* Enable vertical scroll */ + overflow-x: hidden; + /* background-color: red; */ + padding-right: 0.5vw; +} + +.Item { + display: flex; + flex-direction: column; + background-color: rgba(242, 242, 242, 0.05); + border-radius: 0.3vw; + font-family: "Poppins", sans-serif; + width: 100%; + /* ahora se adapta al grid */ +} + +.Item-Info { + display: flex; + justify-content: space-between; + padding: 0.3vw; +} + +.Item-Label { + color: rgba(242, 242, 242, 1); + font-weight: 500; + font-size: 0.8vw; +} + +.Item-Price { + background: rgba(242, 242, 242, 0.1); + padding: 0.1vw 0.2vw; + font-weight: 600; + font-size: 0.6vw; + color: white; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.15vw; +} + +.Item-Cart { + display: flex; + background: rgba(242, 242, 242, 0.1); + justify-content: center; + align-items: center; + padding: 0.2vw; + gap: 0.5vw; + border-bottom-left-radius: 0.3vw; + border-bottom-right-radius: 0.3vw; +} + +.Item-Cart-Icon { + color: rgba(242, 242, 242, 1); + font-size: 0.8vw; +} + +.Item-Cart-Label { + color: rgba(242, 242, 242, 1); + font-size: 0.7vw; +} + +.Item-Image { + padding: 1vw 0; + display: flex; + justify-content: center; + align-items: center; +} + +.Item-Image img { + width: 4vw; + height: 4vw; +} + +.Item:hover { + background: linear-gradient(180deg, rgba(251, 155, 4, 0.1) 0%, rgba(251, 155, 4, 0) 100%); + transition: background 0.3s ease-in-out; + box-shadow: 0px 0px 4px 0px rgba(251, 155, 4, 0.25) inset; + border: 1px solid rgba(251, 155, 4, 0.5); + cursor: pointer; +} + +.Item:hover .Item-Cart { + background: rgba(251, 155, 4, 1); + transition: background 0.5s ease-in-out; +} + +.Item:hover .Item-Cart-Icon, +.Item:hover .Item-Cart-Label { + color: rgba(22, 22, 22, 1); + font-weight: 600; +} + +.Right-Shop-End { + /* background: red; */ + display: flex; + flex-direction: column; + height: 10vh; + justify-content: flex-end; +} + +.Right-Shop-End-Button { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5vw; + /* background-color: red; */ +} + +.Cash, +.Bank { + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(242, 242, 242, 0.1); + width: 50%; + color: rgba(242, 242, 242, 0.349); + padding: 0.4vw 0.3vw; + border-radius: 0.3vw; + font-family: "Poppins", sans-serif; + gap: 0.5vw; + font-weight: 600; + font-size: 0.8vw; +} + +#decrease, +#increase { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + background-color: rgba(242, 242, 242, 0.1); + height: 1.2vw; + width: 1.2vw; + border-radius: 0.1vw; + font-size: 0.8vw; +} + +#count { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + width: 2.5vw; + font-size: 0.8vw; + font-family: "Poppins", sans-serif; + text-align: center; +} + +.Count-Remove { + display: flex; + align-items: center; + justify-content: center; + margin-left: 1vw; + margin-right: 1vw; + background-color: rgba(244, 91, 105, 0.2); + height: 1.2vw; + width: 1.2vw; + border-radius: 0.1vw; +} + +.Count-Remove i { + color: rgba(244, 91, 105, 1); + font-size: 0.8vw; +} + +.Right-Shop-End-Price { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.4vw 0vw; +} + +.Right-Shop-End-Price span { + font-family: "Poppins", sans-serif; + color: rgba(242, 242, 242, 1); + font-weight: 600; + font-size: 0.9vw; +} + +.Basket-Empty { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; +} + +.Basket-Empty i { + font-size: 5vw; + color: rgba(242, 242, 242, 0.281); +} + +.Basket-Empty span { + text-align: center; + color: rgba(242, 242, 242, 0.281); + font-family: "Poppins", sans-serif; +} + +.empty-basket-title { + margin-top: 0.4vw; + text-transform: uppercase; + + font-size: 1vw; + font-weight: 600; +} + +.empty-basket-subtitle { + font-size: 0.7vw; + margin-top: 0.2vw; + font-weight: 300; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +::-webkit-scrollbar { + width: 0.2vw; + height: 0.35vw; + border-radius: 100vw; +} + +::-webkit-scrollbar-track { + background: rgba(251, 155, 4, 0.2); +} + +::-webkit-scrollbar-thumb { + background: rgba(251, 155, 4, 1); + height: 0.3vw; + border-radius: 100vw; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.8); + height: 0.8vw; + border-radius: 100vw; +} + +::-webkit-scrollbar-thumb:active { + background: rgba(255, 255, 255, 0.8); + height: 0.8vw; + border-radius: 100vw; +} + +::-webkit-scrollbar-thumb:focus { + background: rgba(255, 255, 255, 0.8); + height: 0.3vw; + border-radius: 100vw; +} diff --git a/[esx_addons]/esx_shops/server/main.lua b/[esx_addons]/esx_shops/server/main.lua index 86a7c633..407c1442 100644 --- a/[esx_addons]/esx_shops/server/main.lua +++ b/[esx_addons]/esx_shops/server/main.lua @@ -1,32 +1,41 @@ -function GetItemFromShop(itemName, zone) - local zoneItems = Config.Zones[zone].Items - local item = nil +---@diagnostic disable: undefined-global +local ESX = exports['es_extended']:getSharedObject() - for _, itemData in pairs(zoneItems) do - if itemData.name == itemName then - item = itemData - break - end - end +local function buildItemIndex() + local index = {} + for _, item in ipairs(Config.Items or {}) do + index[item.name] = item + end + return index +end - if not item then - return false - end +local itemIndex = buildItemIndex() - return true,item.price, item.label -end +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() ~= resourceName then return end + itemIndex = buildItemIndex() +end) -RegisterServerEvent('esx_shops:buyItem') -AddEventHandler('esx_shops:buyItem', function(itemName, amount, zone) - local source = source - local xPlayer = ESX.GetPlayerFromId(source) - local Exists, price, label = GetItemFromShop(itemName, zone) - amount = ESX.Math.Round(amount) +local function calculateCartTotal(cart) + local total = 0 + for _, entry in ipairs(cart or {}) do + local def = itemIndex[entry.name] + if def then + local qty = tonumber(entry.amount) or 0 + if qty > 0 then + total = total + (def.price * qty) + end + end + end + return total +end - if amount < 0 then - print(('[^3WARNING^7] Player ^5%s^7 attempted to exploit the shop!'):format(source)) - return - end +ESX.RegisterServerCallback('esx_shops:purchase', function(source, cb, data) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then + cb(false, _U('player_not_found')) + return + end local method = data and data.method or 'cash' local cart = data and data.cart or {}