Merge development into master: shared Web UI runtime, bridge-driven UIs, and server-authoritative store flow #1
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_handleUIEvents.sqf
|
* File: fnc_handleUIEvents.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-01-28
|
* Date: 2026-01-28
|
||||||
* Last Update: 2026-02-06
|
* Last Update: 2026-03-11
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -32,12 +32,9 @@ diag_log format ["[FORGE:Client:Store] Handling UI event: %1 with data: %2", _ev
|
|||||||
|
|
||||||
switch (_event) do {
|
switch (_event) do {
|
||||||
case "store::close": { closeDialog 1; };
|
case "store::close": { closeDialog 1; };
|
||||||
case "store::ready": {
|
case "store::ready": { GVAR(StoreUIBridge) call ["handleReady", [_control]]; };
|
||||||
GVAR(StoreUIBridge) call ["handleReady", [_control]];
|
case "store::category::request": { GVAR(StoreUIBridge) call ["handleCategoryRequest", [_data]]; };
|
||||||
};
|
case "store::checkout::request": { GVAR(StoreUIBridge) call ["handleCheckoutRequest", [_data]]; };
|
||||||
case "store::checkout::request": {
|
|
||||||
GVAR(StoreUIBridge) call ["handleCheckoutRequest", [_data]];
|
|
||||||
};
|
|
||||||
default { hint format ["Unhandled UI event: %1", _event]; };
|
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,11 @@
|
|||||||
* File: fnc_initStoreClass.sqf
|
* File: fnc_initStoreClass.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-01-28
|
* Date: 2026-01-28
|
||||||
* Last Update: 2026-02-06
|
* Last Update: 2026-03-11
|
||||||
* Public: Yes
|
* Public: Yes
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
* Initializes the store class for managing store data.
|
* Initializes the store class for managing store data.
|
||||||
* Provides methods for loading and applying store data.
|
|
||||||
*
|
*
|
||||||
* Arguments:
|
* Arguments:
|
||||||
* None
|
* None
|
||||||
@ -22,9 +21,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(StoreClass) = createHashMapObject [[
|
GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [
|
||||||
["#type", "IStoreClass"],
|
["#type", "StoreBaseClass"],
|
||||||
["#create", {
|
["#create", compileFinal {
|
||||||
_self set ["uid", getPlayerUID player];
|
_self set ["uid", getPlayerUID player];
|
||||||
_self set ["store", createHashMap];
|
_self set ["store", createHashMap];
|
||||||
_self set ["workspace", createHashMapFromArray [
|
_self set ["workspace", createHashMapFromArray [
|
||||||
@ -37,10 +36,10 @@ GVAR(StoreClass) = createHashMapObject [[
|
|||||||
_self set ["isLoaded", false];
|
_self set ["isLoaded", false];
|
||||||
_self set ["lastSave", time];
|
_self set ["lastSave", time];
|
||||||
|
|
||||||
systemChat format ["Store class initialized for %1", (name player)];
|
systemChat format ["Store class initialized for %1", name player];
|
||||||
diag_log "[FORGE:Client:Store] Store Class Initialized!";
|
diag_log "[FORGE:Client:Store] Store Class Initialized!";
|
||||||
}],
|
}],
|
||||||
["buildUIPayload", {
|
["buildUIPayload", compileFinal {
|
||||||
private _workspace = _self getOrDefault ["workspace", createHashMap];
|
private _workspace = _self getOrDefault ["workspace", createHashMap];
|
||||||
|
|
||||||
createHashMapFromArray [
|
createHashMapFromArray [
|
||||||
@ -58,7 +57,38 @@ GVAR(StoreClass) = createHashMapObject [[
|
|||||||
]],
|
]],
|
||||||
["cartItems", []]
|
["cartItems", []]
|
||||||
]
|
]
|
||||||
}]
|
}],
|
||||||
]];
|
["formatPriceValue", compileFinal {
|
||||||
|
params [["_priceValue", 0, [0]]];
|
||||||
|
|
||||||
|
format ["$%1", [_priceValue max 0] call BIS_fnc_numberText]
|
||||||
|
}],
|
||||||
|
["calculateItemPrice", compileFinal {
|
||||||
|
params [
|
||||||
|
["_cfg", configNull, [configNull]],
|
||||||
|
["_isVehicle", false, [false]]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isNull _cfg) exitWith { "$50" };
|
||||||
|
|
||||||
|
private _mass = 0;
|
||||||
|
private _priceValue = 0;
|
||||||
|
|
||||||
|
if (_isVehicle) then {
|
||||||
|
_priceValue = getNumber (_cfg >> "cost");
|
||||||
|
} else {
|
||||||
|
_mass = getNumber (_cfg >> "ItemInfo" >> "mass");
|
||||||
|
if (_mass <= 0) then {
|
||||||
|
_mass = getNumber (_cfg >> "mass");
|
||||||
|
};
|
||||||
|
|
||||||
|
_priceValue = ceil ((_mass max 0) * 0.1);
|
||||||
|
};
|
||||||
|
|
||||||
|
_priceValue = _priceValue max 50;
|
||||||
|
_self call ["formatPriceValue", [_priceValue]]
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(StoreClass) = createHashMapObject [GVAR(StoreBaseClass)];
|
||||||
GVAR(StoreClass)
|
GVAR(StoreClass)
|
||||||
|
|||||||
@ -4,15 +4,19 @@
|
|||||||
* File: fnc_initStoreUIBridge.sqf
|
* File: fnc_initStoreUIBridge.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-03-10
|
* Date: 2026-03-10
|
||||||
|
* Last Update: 2026-03-11
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
* Initializes the store UI bridge for browser control state and event routing.
|
* Initializes the store UI bridge for browser control state, event routing, and catalog queries.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||||
["#type", "StoreUIBridgeBaseClass"],
|
["#type", "StoreUIBridgeBaseClass"],
|
||||||
|
["#create", compileFinal {
|
||||||
|
_self set ["catalogCache", createHashMap];
|
||||||
|
}],
|
||||||
["getActiveBrowserControl", compileFinal {
|
["getActiveBrowserControl", compileFinal {
|
||||||
private _display = uiNamespace getVariable ["RscStore", displayNull];
|
private _display = uiNamespace getVariable ["RscStore", displayNull];
|
||||||
if (isNull _display) exitWith { controlNull };
|
if (isNull _display) exitWith { controlNull };
|
||||||
@ -20,11 +24,7 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_display displayCtrl 1004
|
_display displayCtrl 1004
|
||||||
}],
|
}],
|
||||||
["execBridge", compileFinal {
|
["execBridge", compileFinal {
|
||||||
params [
|
params [["_control", controlNull, [controlNull]], ["_fnName", "", [""]], ["_payload", createHashMap, [createHashMap]]];
|
||||||
["_control", controlNull, [controlNull]],
|
|
||||||
["_fnName", "", [""]],
|
|
||||||
["_payload", createHashMap, [createHashMap]]
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isNull _control || { _fnName isEqualTo "" }) exitWith { false };
|
if (isNull _control || { _fnName isEqualTo "" }) exitWith { false };
|
||||||
|
|
||||||
@ -34,18 +34,12 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["sendBridgeEvent", compileFinal {
|
["sendBridgeEvent", compileFinal {
|
||||||
params [
|
params [["_event", "", [""]], ["_data", createHashMap, [createHashMap]], ["_control", controlNull, [controlNull]]];
|
||||||
["_event", "", [""]],
|
|
||||||
["_data", createHashMap, [createHashMap]],
|
|
||||||
["_control", controlNull, [controlNull]]
|
|
||||||
];
|
|
||||||
|
|
||||||
if (_event isEqualTo "") exitWith { false };
|
if (_event isEqualTo "") exitWith { false };
|
||||||
|
|
||||||
private _targetControl = _control;
|
private _targetControl = _control;
|
||||||
if (isNull _targetControl) then {
|
if (isNull _targetControl) then { _targetControl = _self call ["getActiveBrowserControl", []]; };
|
||||||
_targetControl = _self call ["getActiveBrowserControl", []];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNull _targetControl) exitWith { false };
|
if (isNull _targetControl) exitWith { false };
|
||||||
|
|
||||||
@ -54,6 +48,219 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["data", _data]
|
["data", _data]
|
||||||
]]]
|
]]]
|
||||||
}],
|
}],
|
||||||
|
["isVisibleConfig", compileFinal {
|
||||||
|
params [["_cfg", configNull, [configNull]]];
|
||||||
|
|
||||||
|
isClass _cfg
|
||||||
|
&& { getNumber (_cfg >> "scope") >= 2 }
|
||||||
|
&& { (getText (_cfg >> "displayName")) isNotEqualTo "" }
|
||||||
|
}],
|
||||||
|
["buildDescription", compileFinal {
|
||||||
|
params [["_cfg", configNull, [configNull]], ["_fallback", "", [""]]];
|
||||||
|
|
||||||
|
private _description = getText (_cfg >> "descriptionShort");
|
||||||
|
if (_description isEqualTo "") then { _description = _fallback; };
|
||||||
|
|
||||||
|
_description
|
||||||
|
}],
|
||||||
|
["buildItem", compileFinal {
|
||||||
|
params [["_cfg", configNull, [configNull]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]], ["_imageField", "picture", [""]], ["_isVehicle", false, [false]]];
|
||||||
|
|
||||||
|
if (isNull _cfg) exitWith { createHashMap };
|
||||||
|
|
||||||
|
private _className = configName _cfg;
|
||||||
|
private _displayName = getText (_cfg >> "displayName");
|
||||||
|
private _picture = getText (_cfg >> _imageField);
|
||||||
|
if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then { _picture = getText (_cfg >> "picture"); };
|
||||||
|
|
||||||
|
createHashMapFromArray [
|
||||||
|
["className", _className],
|
||||||
|
["code", _className],
|
||||||
|
["name", _displayName],
|
||||||
|
["description", _self call ["buildDescription", [_cfg, _fallbackDescription]]],
|
||||||
|
["price", GVAR(StoreClass) call ["calculateItemPrice", [_cfg, _isVehicle]]],
|
||||||
|
["image", _picture],
|
||||||
|
["type", _typeLabel]
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||||
|
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||||
|
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo _itemInfoType }
|
||||||
|
) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||||
|
|
||||||
|
_items
|
||||||
|
}],
|
||||||
|
["appendCfgWeaponsByType", compileFinal {
|
||||||
|
params [["_items", [], [[]]], ["_weaponType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||||
|
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { getNumber (_cfg >> "type") isEqualTo _weaponType }
|
||||||
|
) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||||
|
|
||||||
|
_items
|
||||||
|
}],
|
||||||
|
["appendCfgVehiclesByKind", compileFinal {
|
||||||
|
params [["_items", [], [[]]], ["_baseClass", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||||
|
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
private _className = configName _cfg;
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||||
|
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||||
|
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||||
|
&& { _className isKindOf [_baseClass, configFile >> "CfgVehicles"] }
|
||||||
|
) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||||
|
|
||||||
|
_items
|
||||||
|
}],
|
||||||
|
["scanCategoryItems", compileFinal {
|
||||||
|
params [["_category", "", [""]]];
|
||||||
|
|
||||||
|
private _categoryKey = toLowerANSI _category;
|
||||||
|
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||||
|
if (isNil QGVAR(StoreClass)) exitWith { [] };
|
||||||
|
|
||||||
|
private _items = [];
|
||||||
|
|
||||||
|
switch (_categoryKey) do {
|
||||||
|
case "uniforms": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 801, "Uniform", "Live uniform entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "headgear": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 605, "Headgear", "Live headgear entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "vests": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 701, "Vest", "Live vest entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "facewear": {
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
if (_self call ["isVisibleConfig", [_cfg]]) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, "Facewear", "Live facewear entry generated from the game inventory."]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgGlasses"));
|
||||||
|
};
|
||||||
|
case "ammo": {
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
if (_self call ["isVisibleConfig", [_cfg]]) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, "Magazine", "Live ammunition entry generated from the game inventory."]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgMagazines"));
|
||||||
|
};
|
||||||
|
case "items": {
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
private _className = configName _cfg;
|
||||||
|
private _itemType = [_className] call BIS_fnc_itemType;
|
||||||
|
private _group = _itemType param [0, ""];
|
||||||
|
private _kind = _itemType param [1, ""];
|
||||||
|
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { _group in ["Item", "Equipment"] }
|
||||||
|
&& { !(_kind in ["Uniform", "Vest", "Headgear"]) }
|
||||||
|
) then {
|
||||||
|
private _typeLabel = [_kind, "Item"] select (_kind isEqualTo "");
|
||||||
|
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, "Live utility entry generated from the game inventory."]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||||
|
};
|
||||||
|
case "primary": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByType", [_items, 1, "Primary Weapon", "Live primary weapon entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "handgun": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByType", [_items, 2, "Handgun", "Live sidearm entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "secondary": {
|
||||||
|
_items = _self call ["appendCfgWeaponsByType", [_items, 4, "Launcher", "Live launcher entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "cars": {
|
||||||
|
_items = _self call ["appendCfgVehiclesByKind", [_items, "Car", "Vehicle", "Live wheeled vehicle entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "armor": {
|
||||||
|
_items = _self call ["appendCfgVehiclesByKind", [_items, "Tank", "Vehicle", "Live armored vehicle entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "helis": {
|
||||||
|
_items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "planes": {
|
||||||
|
_items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "naval": {
|
||||||
|
_items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]];
|
||||||
|
};
|
||||||
|
case "other": {
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
private _className = configName _cfg;
|
||||||
|
private _isSupportedVehicle = _className isKindOf ["AllVehicles", configFile >> "CfgVehicles"];
|
||||||
|
private _isKnownCategory =
|
||||||
|
_className isKindOf ["Car", configFile >> "CfgVehicles"]
|
||||||
|
|| { _className isKindOf ["Tank", configFile >> "CfgVehicles"] }
|
||||||
|
|| { _className isKindOf ["Helicopter", configFile >> "CfgVehicles"] }
|
||||||
|
|| { _className isKindOf ["Plane", configFile >> "CfgVehicles"] }
|
||||||
|
|| { _className isKindOf ["Ship", configFile >> "CfgVehicles"] };
|
||||||
|
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { _isSupportedVehicle }
|
||||||
|
&& { !_isKnownCategory }
|
||||||
|
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||||
|
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||||
|
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||||
|
) then {
|
||||||
|
_items pushBack (_self call ["buildItem", [
|
||||||
|
_cfg,
|
||||||
|
"Special Vehicle",
|
||||||
|
"Live specialty vehicle entry generated from the game inventory.",
|
||||||
|
"editorPreview",
|
||||||
|
true
|
||||||
|
]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private _sortedItems = _items apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] };
|
||||||
|
|
||||||
|
_sortedItems sort true;
|
||||||
|
_sortedItems apply { _x select 1 }
|
||||||
|
}],
|
||||||
|
["buildCategoryItems", compileFinal {
|
||||||
|
params [["_category", "", [""]]];
|
||||||
|
|
||||||
|
private _categoryKey = toLowerANSI _category;
|
||||||
|
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||||
|
|
||||||
|
private _catalogCache = _self getOrDefault ["catalogCache", createHashMap];
|
||||||
|
if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey };
|
||||||
|
|
||||||
|
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
||||||
|
_catalogCache set [_categoryKey, _items];
|
||||||
|
_self set ["catalogCache", _catalogCache];
|
||||||
|
|
||||||
|
_items
|
||||||
|
}],
|
||||||
["handleReady", compileFinal {
|
["handleReady", compileFinal {
|
||||||
params [["_control", controlNull, [controlNull]]];
|
params [["_control", controlNull, [controlNull]]];
|
||||||
|
|
||||||
@ -65,23 +272,40 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
_self call ["sendBridgeEvent", ["store::hydrate", _payload, _control]];
|
_self call ["sendBridgeEvent", ["store::hydrate", _payload, _control]];
|
||||||
}],
|
}],
|
||||||
|
["handleCategoryRequest", compileFinal {
|
||||||
|
params [["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
private _category = toLowerANSI (_data getOrDefault ["category", ""]);
|
||||||
|
if (_category isEqualTo "") exitWith {
|
||||||
|
_self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [
|
||||||
|
["message", "No store category was provided."]
|
||||||
|
]]];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNil QGVAR(StoreClass)) exitWith {
|
||||||
|
_self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [
|
||||||
|
["category", _category],
|
||||||
|
["message", "Store data is unavailable."]
|
||||||
|
]]];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _items = _self call ["buildCategoryItems", [_category]];
|
||||||
|
|
||||||
|
diag_log format ["[FORGE:Client:Store] Category request handled for %1 with %2 item(s).", _category, count _items];
|
||||||
|
|
||||||
|
_self call ["sendBridgeEvent", ["store::category::hydrate", createHashMapFromArray [
|
||||||
|
["category", _category],
|
||||||
|
["items", _items]
|
||||||
|
]]];
|
||||||
|
}],
|
||||||
["handleCheckoutRequest", compileFinal {
|
["handleCheckoutRequest", compileFinal {
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
params [["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
private _items = _data getOrDefault ["items", []];
|
private _items = _data getOrDefault ["items", []];
|
||||||
private _message = format [
|
private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s).", count _items];
|
||||||
"Checkout integration is not wired yet. Received %1 queued line(s).",
|
|
||||||
count _items
|
|
||||||
];
|
|
||||||
|
|
||||||
diag_log format [
|
diag_log format ["[FORGE:Client:Store] Checkout request received: %1", _data];
|
||||||
"[FORGE:Client:Store] Checkout request received: %1",
|
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [["message", _message]]]];
|
||||||
_data
|
|
||||||
];
|
|
||||||
|
|
||||||
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [
|
|
||||||
["message", _message]
|
|
||||||
]]];
|
|
||||||
}]
|
}]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_openUI.sqf
|
* File: fnc_openUI.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-01-28
|
* Date: 2026-01-28
|
||||||
* Last Update: 2026-02-06
|
* Last Update: 2026-03-11
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
return sendEvent("store::checkout::request", payload);
|
return sendEvent("store::checkout::request", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestCategory(payload) {
|
||||||
|
return sendEvent("store::category::request", payload);
|
||||||
|
}
|
||||||
|
|
||||||
function notifyReady() {
|
function notifyReady() {
|
||||||
return sendEvent("store::ready", { loaded: true });
|
return sendEvent("store::ready", { loaded: true });
|
||||||
}
|
}
|
||||||
@ -60,6 +64,22 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event === "store::category::hydrate") {
|
||||||
|
store.hydrateCategoryItems(payloadData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === "store::category::failure") {
|
||||||
|
store.finishCategoryRequest(payloadData.category || "");
|
||||||
|
if (StorefrontApp.actions) {
|
||||||
|
StorefrontApp.actions.showNotice(
|
||||||
|
"error",
|
||||||
|
payloadData.message || "Category request failed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event === "store::checkout::failure") {
|
if (event === "store::checkout::failure") {
|
||||||
store.setIsCheckingOut(false);
|
store.setIsCheckingOut(false);
|
||||||
if (StorefrontApp.actions) {
|
if (StorefrontApp.actions) {
|
||||||
@ -75,6 +95,7 @@
|
|||||||
sendEvent,
|
sendEvent,
|
||||||
requestClose,
|
requestClose,
|
||||||
requestCheckout,
|
requestCheckout,
|
||||||
|
requestCategory,
|
||||||
notifyReady,
|
notifyReady,
|
||||||
receive,
|
receive,
|
||||||
};
|
};
|
||||||
@ -82,6 +103,7 @@
|
|||||||
window.StoreUIBridge = {
|
window.StoreUIBridge = {
|
||||||
requestClose,
|
requestClose,
|
||||||
requestCheckout,
|
requestCheckout,
|
||||||
|
requestCategory,
|
||||||
notifyReady,
|
notifyReady,
|
||||||
receive,
|
receive,
|
||||||
receiveHydrate: (data) => receive("store::hydrate", data),
|
receiveHydrate: (data) => receive("store::hydrate", data),
|
||||||
|
|||||||
@ -403,6 +403,7 @@ ${scopeSelector} .store-toast.is-error {
|
|||||||
CategoryGrid,
|
CategoryGrid,
|
||||||
SubcategoryGrid,
|
SubcategoryGrid,
|
||||||
ProductGrid,
|
ProductGrid,
|
||||||
|
CatalogPager,
|
||||||
} = StorefrontApp.componentFns;
|
} = StorefrontApp.componentFns;
|
||||||
|
|
||||||
if (state.view === "weapons" || state.view === "vehicles") {
|
if (state.view === "weapons" || state.view === "vehicles") {
|
||||||
@ -425,23 +426,43 @@ ${scopeSelector} .store-toast.is-error {
|
|||||||
|
|
||||||
if (state.view === "items") {
|
if (state.view === "items") {
|
||||||
const items = getters.getVisibleItems(state, catalog);
|
const items = getters.getVisibleItems(state, catalog);
|
||||||
|
const pagedItems = getters.getVisibleItemsPage(state, catalog);
|
||||||
|
const pagination = getters.getCatalogPagination(state, catalog);
|
||||||
const quantityByCode = state.cartItems.reduce((acc, item) => {
|
const quantityByCode = state.cartItems.reduce((acc, item) => {
|
||||||
acc[item.code] = item.quantity;
|
acc[item.code] = item.quantity;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
const selectionKey = String(
|
||||||
|
getters.getSelectionKey(state) || "",
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
return ProductGrid(
|
return [
|
||||||
items.length > 0
|
ProductGrid(
|
||||||
? items.map((item) =>
|
state.isCatalogLoading &&
|
||||||
ProductCard(item, quantityByCode[item.code] || 0),
|
state.catalogRequestKey === selectionKey &&
|
||||||
)
|
items.length === 0
|
||||||
: EmptyStateCard({
|
? EmptyStateCard({
|
||||||
title: "No matching products",
|
title: "Loading inventory",
|
||||||
copy: "Your search filter excluded the available preview items for this category.",
|
copy: "Pulling live category items from the game engine.",
|
||||||
actionLabel: "Clear Search",
|
})
|
||||||
onAction: () => actions.clearSearch(),
|
: items.length > 0
|
||||||
}),
|
? pagedItems.map((item) =>
|
||||||
);
|
ProductCard(
|
||||||
|
item,
|
||||||
|
quantityByCode[item.code] || 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: EmptyStateCard({
|
||||||
|
title: "No category items",
|
||||||
|
copy: state.searchQuery
|
||||||
|
? "Your search filter excluded the live inventory returned for this category."
|
||||||
|
: "The game engine did not return any items for this category yet.",
|
||||||
|
actionLabel: "Clear Search",
|
||||||
|
onAction: () => actions.clearSearch(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
items.length > 0 ? CatalogPager(pagination) : null,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = getters.getVisibleCategoryCards(state, catalog);
|
const items = getters.getVisibleCategoryCards(state, catalog);
|
||||||
|
|||||||
@ -2,12 +2,24 @@
|
|||||||
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
||||||
const { h, ensureScopedStyle } = StorefrontApp.runtime;
|
const { h, ensureScopedStyle } = StorefrontApp.runtime;
|
||||||
const actions = StorefrontApp.actions;
|
const actions = StorefrontApp.actions;
|
||||||
|
const media = StorefrontApp.media;
|
||||||
const scopeAttr = "data-ui-store-cards";
|
const scopeAttr = "data-ui-store-cards";
|
||||||
const scopeSelector = `[${scopeAttr}]`;
|
const scopeSelector = `[${scopeAttr}]`;
|
||||||
const cardsCss = `
|
const cardsCss = `
|
||||||
|
${scopeSelector}.catalog-grid-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector}.catalog-pager-shell {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
${scopeSelector} .catalog-grid {
|
${scopeSelector} .catalog-grid {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@ -105,16 +117,20 @@ ${scopeSelector} .empty-state-copy {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .product-copy {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-card {
|
${scopeSelector} .product-card {
|
||||||
min-height: 20rem;
|
min-height: 15.5rem;
|
||||||
padding: 0.95rem;
|
padding: 0.8rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.9rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-image {
|
${scopeSelector} .product-image {
|
||||||
height: 9.5rem;
|
height: 5.9rem;
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.95rem;
|
||||||
border: 1px dashed rgb(18 54 93 / 0.24);
|
border: 1px dashed rgb(18 54 93 / 0.24);
|
||||||
background: linear-gradient(135deg, rgb(235 240 245) 0%, rgb(221 228 235) 100%);
|
background: linear-gradient(135deg, rgb(235 240 245) 0%, rgb(221 228 235) 100%);
|
||||||
@ -125,18 +141,26 @@ ${scopeSelector} .product-image {
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
letter-spacing: 0.16em;
|
letter-spacing: 0.16em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .product-image-asset {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-meta {
|
${scopeSelector} .product-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-name {
|
${scopeSelector} .product-name {
|
||||||
font-size: 1rem;
|
font-size: 0.96rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--store-text-main);
|
color: var(--store-text-main);
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-footer {
|
${scopeSelector} .product-footer {
|
||||||
@ -148,7 +172,7 @@ ${scopeSelector} .product-footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
${scopeSelector} .product-price {
|
${scopeSelector} .product-price {
|
||||||
font-size: 1rem;
|
font-size: 0.96rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--store-success);
|
color: var(--store-success);
|
||||||
}
|
}
|
||||||
@ -173,6 +197,49 @@ ${scopeSelector} .empty-state {
|
|||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .catalog-pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 0.55rem 0.9rem 0.75rem;
|
||||||
|
border-top: 1px solid var(--store-accent-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .catalog-pager-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .catalog-pager-summary {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: var(--store-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .catalog-pager-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .catalog-pager-page {
|
||||||
|
min-width: 5.75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--store-accent);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
${scopeSelector} .product-copy {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1440px) {
|
@media (max-width: 1440px) {
|
||||||
${scopeSelector} .catalog-grid.is-categories,
|
${scopeSelector} .catalog-grid.is-categories,
|
||||||
${scopeSelector} .catalog-grid.is-products {
|
${scopeSelector} .catalog-grid.is-products {
|
||||||
@ -194,13 +261,56 @@ ${scopeSelector} .empty-state {
|
|||||||
function createGrid(className, children) {
|
function createGrid(className, children) {
|
||||||
ensureScopedStyle("storefront-cards", cardsCss);
|
ensureScopedStyle("storefront-cards", cardsCss);
|
||||||
|
|
||||||
|
if (
|
||||||
|
className === "is-products" &&
|
||||||
|
media &&
|
||||||
|
typeof media.scheduleTextureObservation === "function"
|
||||||
|
) {
|
||||||
|
media.scheduleTextureObservation();
|
||||||
|
}
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"div",
|
"div",
|
||||||
{ [scopeAttr]: "" },
|
{
|
||||||
h("div", { className: `catalog-grid ${className}` }, children),
|
[scopeAttr]: "",
|
||||||
|
className: "catalog-grid-shell",
|
||||||
|
},
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `catalog-grid ${className}`,
|
||||||
|
"data-preserve-scroll-id": "catalog-grid",
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDescription(description, fallbackValue) {
|
||||||
|
const rawDescription = String(description || "").trim();
|
||||||
|
if (!rawDescription) {
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlDescription = rawDescription
|
||||||
|
.replace(/<\s*br\s*\/?\s*>/gi, "\n")
|
||||||
|
.replace(/<\/\s*p\s*>/gi, "\n")
|
||||||
|
.replace(/<\s*li\s*>/gi, "- ")
|
||||||
|
.replace(/<\/\s*li\s*>/gi, "\n");
|
||||||
|
const scratch = document.createElement("div");
|
||||||
|
scratch.innerHTML = htmlDescription;
|
||||||
|
|
||||||
|
const textDescription = String(
|
||||||
|
scratch.textContent || scratch.innerText || "",
|
||||||
|
)
|
||||||
|
.replace(/\u00a0/g, " ")
|
||||||
|
.replace(/[ \t]+\n/g, "\n")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return textDescription || fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
StorefrontApp.componentFns.CategoryCard = function CategoryCard(category) {
|
StorefrontApp.componentFns.CategoryCard = function CategoryCard(category) {
|
||||||
return h(
|
return h(
|
||||||
"button",
|
"button",
|
||||||
@ -248,21 +358,58 @@ ${scopeSelector} .empty-state {
|
|||||||
item,
|
item,
|
||||||
quantityInCart,
|
quantityInCart,
|
||||||
) {
|
) {
|
||||||
|
const textureState =
|
||||||
|
media && typeof media.getTextureState === "function"
|
||||||
|
? media.getTextureState(item.image)
|
||||||
|
: { isVisible: true };
|
||||||
|
const textureSource =
|
||||||
|
media && typeof media.getTextureSource === "function"
|
||||||
|
? media.getTextureSource(item.image)
|
||||||
|
: "";
|
||||||
|
const description = formatDescription(
|
||||||
|
item.description,
|
||||||
|
item.className || item.code,
|
||||||
|
);
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"article",
|
"article",
|
||||||
{ className: "product-card" },
|
{ className: "product-card" },
|
||||||
h("div", { className: "product-image" }, "Image Placeholder"),
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "product-image",
|
||||||
|
"data-store-texture-path": item.image || "",
|
||||||
|
},
|
||||||
|
textureSource
|
||||||
|
? h("img", {
|
||||||
|
className: "product-image-asset",
|
||||||
|
src: textureSource,
|
||||||
|
alt: item.name,
|
||||||
|
loading: "lazy",
|
||||||
|
})
|
||||||
|
: textureState.isVisible
|
||||||
|
? "Loading Image"
|
||||||
|
: "Image Placeholder",
|
||||||
|
),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "product-meta" },
|
{ className: "product-meta" },
|
||||||
h("span", { className: "product-code" }, item.code),
|
h(
|
||||||
|
"span",
|
||||||
|
{ className: "product-code" },
|
||||||
|
item.type || item.code || item.className,
|
||||||
|
),
|
||||||
h("strong", { className: "product-name" }, item.name),
|
h("strong", { className: "product-name" }, item.name),
|
||||||
),
|
),
|
||||||
h("p", { className: "product-copy" }, item.description),
|
h("p", { className: "product-copy" }, description),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "product-footer" },
|
{ className: "product-footer" },
|
||||||
h("span", { className: "product-price" }, item.price),
|
h(
|
||||||
|
"span",
|
||||||
|
{ className: "product-price" },
|
||||||
|
item.price || "Pending",
|
||||||
|
),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@ -332,4 +479,68 @@ ${scopeSelector} .empty-state {
|
|||||||
StorefrontApp.componentFns.ProductGrid = function ProductGrid(children) {
|
StorefrontApp.componentFns.ProductGrid = function ProductGrid(children) {
|
||||||
return createGrid("is-products", children);
|
return createGrid("is-products", children);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StorefrontApp.componentFns.CatalogPager = function CatalogPager({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
totalItems,
|
||||||
|
}) {
|
||||||
|
ensureScopedStyle("storefront-cards", cardsCss);
|
||||||
|
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
[scopeAttr]: "",
|
||||||
|
className: "catalog-pager-shell",
|
||||||
|
},
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ className: "catalog-pager" },
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ className: "catalog-pager-meta" },
|
||||||
|
h("span", { className: "card-kicker" }, "Catalog Page"),
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{ className: "catalog-pager-summary" },
|
||||||
|
totalItems > 0
|
||||||
|
? `Showing ${startIndex}-${endIndex} of ${totalItems} items`
|
||||||
|
: "No items available",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ className: "catalog-pager-actions" },
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "store-btn store-btn-secondary",
|
||||||
|
disabled: currentPage <= 1,
|
||||||
|
onClick: () => actions.goToPreviousCatalogPage(),
|
||||||
|
},
|
||||||
|
"Previous",
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{ className: "catalog-pager-page" },
|
||||||
|
`Page ${currentPage} / ${totalPages}`,
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "store-btn store-btn-secondary",
|
||||||
|
disabled: currentPage >= totalPages,
|
||||||
|
onClick: () =>
|
||||||
|
actions.goToNextCatalogPage(totalPages),
|
||||||
|
},
|
||||||
|
"Next",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -301,7 +301,10 @@ ${scopeSelector} .cart-empty {
|
|||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "cart-lines" },
|
{
|
||||||
|
className: "cart-lines",
|
||||||
|
"data-preserve-scroll-id": "cart-lines",
|
||||||
|
},
|
||||||
summary.lineCount > 0
|
summary.lineCount > 0
|
||||||
? state.cartItems.map((item) =>
|
? state.cartItems.map((item) =>
|
||||||
h(
|
h(
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
const styleFiles = ["style.css"];
|
const styleFiles = ["style.css"];
|
||||||
const scriptFiles = [
|
const scriptFiles = [
|
||||||
"runtime.js",
|
"runtime.js",
|
||||||
|
"media.js",
|
||||||
"data.js",
|
"data.js",
|
||||||
"logic/store.js",
|
"logic/store.js",
|
||||||
"pages/StoreView.js",
|
"pages/StoreView.js",
|
||||||
|
|||||||
@ -21,10 +21,12 @@
|
|||||||
|
|
||||||
function applySearchQuery(value) {
|
function applySearchQuery(value) {
|
||||||
store.setSearchQuery(String(value || "").trim());
|
store.setSearchQuery(String(value || "").trim());
|
||||||
|
store.resetCatalogPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
store.setSearchQuery("");
|
store.setSearchQuery("");
|
||||||
|
store.resetCatalogPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCart() {
|
function toggleCart() {
|
||||||
@ -52,12 +54,87 @@
|
|||||||
return store.navigateToBreadcrumb(target);
|
return store.navigateToBreadcrumb(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollCatalogToTop() {
|
||||||
|
const catalogGrid = document.querySelector(
|
||||||
|
'[data-preserve-scroll-id="catalog-grid"]',
|
||||||
|
);
|
||||||
|
if (catalogGrid) {
|
||||||
|
catalogGrid.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectCategory(category) {
|
function selectCategory(category) {
|
||||||
store.selectCategory(category);
|
store.selectCategory(category);
|
||||||
|
scrollCatalogToTop();
|
||||||
|
|
||||||
|
if (!["weapons", "vehicles"].includes(String(category || ""))) {
|
||||||
|
requestCategoryItems(category);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSubcategory(subcategory, slotType) {
|
function selectSubcategory(subcategory, slotType) {
|
||||||
store.selectSubcategory(subcategory, slotType);
|
store.selectSubcategory(subcategory, slotType);
|
||||||
|
scrollCatalogToTop();
|
||||||
|
requestCategoryItems(subcategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToCatalogPage(page) {
|
||||||
|
store.setCatalogPageNumber(page);
|
||||||
|
scrollCatalogToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextCatalogPage(totalPages) {
|
||||||
|
const currentPage = Number(store.getCatalogPage() || 1);
|
||||||
|
const lastPage = Math.max(1, Number(totalPages || 1));
|
||||||
|
if (currentPage >= lastPage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToCatalogPage(currentPage + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPreviousCatalogPage() {
|
||||||
|
const currentPage = Number(store.getCatalogPage() || 1);
|
||||||
|
if (currentPage <= 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToCatalogPage(currentPage - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCategoryItems(category) {
|
||||||
|
const categoryKey = String(category || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!categoryKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedItems = store.getCatalogItemsByKey();
|
||||||
|
if (Array.isArray(cachedItems[categoryKey])) {
|
||||||
|
store.finishCategoryRequest("");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.startCategoryRequest(categoryKey);
|
||||||
|
|
||||||
|
const bridge = StorefrontApp.bridge;
|
||||||
|
if (!bridge || typeof bridge.requestCategory !== "function") {
|
||||||
|
store.finishCategoryRequest(categoryKey);
|
||||||
|
showNotice("error", "Store bridge is unavailable.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sent = bridge.requestCategory({ category: categoryKey });
|
||||||
|
if (!sent) {
|
||||||
|
store.finishCategoryRequest(categoryKey);
|
||||||
|
showNotice("error", "Category request bridge is unavailable.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCart(item) {
|
function addToCart(item) {
|
||||||
@ -173,6 +250,9 @@
|
|||||||
navigateToBreadcrumb,
|
navigateToBreadcrumb,
|
||||||
selectCategory,
|
selectCategory,
|
||||||
selectSubcategory,
|
selectSubcategory,
|
||||||
|
goToCatalogPage,
|
||||||
|
goToNextCatalogPage,
|
||||||
|
goToPreviousCatalogPage,
|
||||||
addToCart,
|
addToCart,
|
||||||
incrementCartItem,
|
incrementCartItem,
|
||||||
decrementCartItem,
|
decrementCartItem,
|
||||||
|
|||||||
@ -16,6 +16,13 @@
|
|||||||
[this.getCartOpen, this.setCartOpen] = createSignal(false);
|
[this.getCartOpen, this.setCartOpen] = createSignal(false);
|
||||||
[this.getSearchQuery, this.setSearchQuery] = createSignal("");
|
[this.getSearchQuery, this.setSearchQuery] = createSignal("");
|
||||||
[this.getCartItems, this.setCartItems] = createSignal([]);
|
[this.getCartItems, this.setCartItems] = createSignal([]);
|
||||||
|
[this.getCatalogItemsByKey, this.setCatalogItemsByKey] =
|
||||||
|
createSignal({});
|
||||||
|
[this.getIsCatalogLoading, this.setIsCatalogLoading] =
|
||||||
|
createSignal(false);
|
||||||
|
[this.getCatalogRequestKey, this.setCatalogRequestKey] =
|
||||||
|
createSignal("");
|
||||||
|
[this.getCatalogPage, this.setCatalogPage] = createSignal(1);
|
||||||
[this.getNotice, this.setNotice] = createSignal({
|
[this.getNotice, this.setNotice] = createSignal({
|
||||||
type: "",
|
type: "",
|
||||||
text: "",
|
text: "",
|
||||||
@ -29,6 +36,9 @@
|
|||||||
this.setSelectedCategory("");
|
this.setSelectedCategory("");
|
||||||
this.setSelectedWeaponSlot("");
|
this.setSelectedWeaponSlot("");
|
||||||
this.setSelectedVehicleSlot("");
|
this.setSelectedVehicleSlot("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setCatalogPage(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
openWeaponsRoot() {
|
openWeaponsRoot() {
|
||||||
@ -36,6 +46,9 @@
|
|||||||
this.setSelectedCategory("weapons");
|
this.setSelectedCategory("weapons");
|
||||||
this.setSelectedWeaponSlot("");
|
this.setSelectedWeaponSlot("");
|
||||||
this.setSelectedVehicleSlot("");
|
this.setSelectedVehicleSlot("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setCatalogPage(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
openVehiclesRoot() {
|
openVehiclesRoot() {
|
||||||
@ -43,12 +56,25 @@
|
|||||||
this.setSelectedCategory("vehicles");
|
this.setSelectedCategory("vehicles");
|
||||||
this.setSelectedVehicleSlot("");
|
this.setSelectedVehicleSlot("");
|
||||||
this.setSelectedWeaponSlot("");
|
this.setSelectedWeaponSlot("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setCatalogPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCatalogPage() {
|
||||||
|
this.setCatalogPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCatalogPageNumber(page) {
|
||||||
|
const nextPage = Math.max(1, Number(page || 1));
|
||||||
|
this.setCatalogPage(nextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectCategory(category) {
|
selectCategory(category) {
|
||||||
this.setSelectedCategory(category);
|
this.setSelectedCategory(category);
|
||||||
this.setSelectedWeaponSlot("");
|
this.setSelectedWeaponSlot("");
|
||||||
this.setSelectedVehicleSlot("");
|
this.setSelectedVehicleSlot("");
|
||||||
|
this.setCatalogPage(1);
|
||||||
|
|
||||||
if (category === "weapons") {
|
if (category === "weapons") {
|
||||||
this.openWeaponsRoot();
|
this.openWeaponsRoot();
|
||||||
@ -72,9 +98,71 @@
|
|||||||
this.setSelectedVehicleSlot("");
|
this.setSelectedVehicleSlot("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setCatalogPage(1);
|
||||||
this.setView("items");
|
this.setView("items");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startCategoryRequest(category) {
|
||||||
|
const categoryKey = String(category || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!categoryKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCatalogRequestKey(categoryKey);
|
||||||
|
this.setIsCatalogLoading(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCategoryRequest(category) {
|
||||||
|
const categoryKey = String(category || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const activeKey = String(this.getCatalogRequestKey() || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (!categoryKey || !activeKey || activeKey === categoryKey) {
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateCategoryItems(payload) {
|
||||||
|
const categoryKey = String(payload?.category || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const items = Array.isArray(payload?.items)
|
||||||
|
? payload.items
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!categoryKey) {
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCatalogItemsByKey((currentItemsByKey) =>
|
||||||
|
Object.assign({}, currentItemsByKey, {
|
||||||
|
[categoryKey]: items.map((item) => ({
|
||||||
|
className: String(
|
||||||
|
item.className || item.code || "",
|
||||||
|
),
|
||||||
|
code: String(item.code || item.className || ""),
|
||||||
|
name: String(item.name || item.displayName || ""),
|
||||||
|
description: String(item.description || ""),
|
||||||
|
price: String(item.price || ""),
|
||||||
|
image: String(item.image || ""),
|
||||||
|
type: String(item.type || ""),
|
||||||
|
quantity: Math.max(0, Number(item.quantity || 0)),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.finishCategoryRequest(categoryKey);
|
||||||
|
}
|
||||||
|
|
||||||
navigateToBreadcrumb(target) {
|
navigateToBreadcrumb(target) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case "categories":
|
case "categories":
|
||||||
@ -106,6 +194,10 @@
|
|||||||
);
|
);
|
||||||
this.setCartOpen(false);
|
this.setCartOpen(false);
|
||||||
this.setIsCheckingOut(false);
|
this.setIsCheckingOut(false);
|
||||||
|
this.setCatalogItemsByKey({});
|
||||||
|
this.setCatalogRequestKey("");
|
||||||
|
this.setIsCatalogLoading(false);
|
||||||
|
this.setCatalogPage(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
266
arma/client/addons/store/ui/_site/media.js
Normal file
266
arma/client/addons/store/ui/_site/media.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
(function () {
|
||||||
|
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
||||||
|
const runtime = StorefrontApp.runtime;
|
||||||
|
const MAX_CONCURRENT_TEXTURES = 6;
|
||||||
|
const RERENDER_DELAY_MS = 48;
|
||||||
|
const textureCache = Object.create(null);
|
||||||
|
const textureRequests = Object.create(null);
|
||||||
|
const queuedTexturePaths = [];
|
||||||
|
const queuedTextureLookup = Object.create(null);
|
||||||
|
const visibleTexturePaths = Object.create(null);
|
||||||
|
const observedTextureNodes = new WeakSet();
|
||||||
|
let activeTextureRequests = 0;
|
||||||
|
let observer = null;
|
||||||
|
let observerRoot = null;
|
||||||
|
let rerenderTimer = 0;
|
||||||
|
|
||||||
|
function normalizeTexturePath(path) {
|
||||||
|
let normalizedPath = String(path || "").trim();
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
normalizedPath.startsWith("\\") ||
|
||||||
|
normalizedPath.startsWith("/")
|
||||||
|
) {
|
||||||
|
normalizedPath = normalizedPath.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/\.[A-Za-z0-9]+$/.test(normalizedPath)) {
|
||||||
|
normalizedPath += ".paa";
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBrowserTextureSource(path) {
|
||||||
|
const value = String(path || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
return (
|
||||||
|
value.startsWith("data:image/") ||
|
||||||
|
value.startsWith("blob:") ||
|
||||||
|
value.startsWith("http://") ||
|
||||||
|
value.startsWith("https://")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeTextureSource(path, source) {
|
||||||
|
textureCache[path] = source;
|
||||||
|
|
||||||
|
scheduleRerender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRerender() {
|
||||||
|
if (rerenderTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rerenderTimer = window.setTimeout(() => {
|
||||||
|
rerenderTimer = 0;
|
||||||
|
if (runtime && typeof runtime.rerender === "function") {
|
||||||
|
runtime.rerender();
|
||||||
|
}
|
||||||
|
}, RERENDER_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pumpTextureQueue() {
|
||||||
|
if (
|
||||||
|
typeof A3API === "undefined" ||
|
||||||
|
typeof A3API.RequestTexture !== "function"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
activeTextureRequests < MAX_CONCURRENT_TEXTURES &&
|
||||||
|
queuedTexturePaths.length > 0
|
||||||
|
) {
|
||||||
|
const normalizedPath = queuedTexturePaths.shift();
|
||||||
|
delete queuedTextureLookup[normalizedPath];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!normalizedPath ||
|
||||||
|
textureCache[normalizedPath] !== undefined ||
|
||||||
|
textureRequests[normalizedPath]
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTextureRequests += 1;
|
||||||
|
textureRequests[normalizedPath] = Promise.resolve(
|
||||||
|
A3API.RequestTexture(normalizedPath, 512),
|
||||||
|
)
|
||||||
|
.then((resolvedPath) => {
|
||||||
|
const textureSource = String(resolvedPath || "").trim();
|
||||||
|
|
||||||
|
if (isBrowserTextureSource(textureSource)) {
|
||||||
|
finalizeTextureSource(normalizedPath, textureSource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[Store UI] Ignoring unsupported texture response.",
|
||||||
|
normalizedPath,
|
||||||
|
textureSource,
|
||||||
|
);
|
||||||
|
finalizeTextureSource(normalizedPath, "");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(
|
||||||
|
"[Store UI] Failed to resolve texture.",
|
||||||
|
normalizedPath,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
finalizeTextureSource(normalizedPath, "");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
activeTextureRequests = Math.max(
|
||||||
|
0,
|
||||||
|
activeTextureRequests - 1,
|
||||||
|
);
|
||||||
|
delete textureRequests[normalizedPath];
|
||||||
|
pumpTextureQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueTextureRequest(path) {
|
||||||
|
if (!path || queuedTextureLookup[path] || textureRequests[path]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queuedTextureLookup[path] = true;
|
||||||
|
queuedTexturePaths.push(path);
|
||||||
|
pumpTextureQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTextureVisible(path) {
|
||||||
|
const normalizedPath = normalizeTexturePath(path);
|
||||||
|
if (!normalizedPath || visibleTexturePaths[normalizedPath]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleTexturePaths[normalizedPath] = true;
|
||||||
|
if (
|
||||||
|
!isBrowserTextureSource(textureCache[normalizedPath]) &&
|
||||||
|
!textureRequests[normalizedPath]
|
||||||
|
) {
|
||||||
|
queueTextureRequest(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureObserver() {
|
||||||
|
const currentRoot = document.querySelector(".catalog-grid");
|
||||||
|
if (typeof IntersectionObserver !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer && observerRoot === currentRoot) {
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
observerRoot = currentRoot;
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPath = entry.target.getAttribute(
|
||||||
|
"data-store-texture-path",
|
||||||
|
);
|
||||||
|
markTextureVisible(rawPath);
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: currentRoot,
|
||||||
|
rootMargin: "240px 0px",
|
||||||
|
threshold: 0.01,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeTextureTargets() {
|
||||||
|
const targets = document.querySelectorAll("[data-store-texture-path]");
|
||||||
|
if (targets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeObserver = ensureObserver();
|
||||||
|
targets.forEach((target) => {
|
||||||
|
if (observedTextureNodes.has(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observedTextureNodes.add(target);
|
||||||
|
|
||||||
|
const rawPath = target.getAttribute("data-store-texture-path");
|
||||||
|
if (!activeObserver) {
|
||||||
|
markTextureVisible(rawPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeObserver.observe(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleTextureObservation() {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
observeTextureTargets();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextureState(path) {
|
||||||
|
const normalizedPath = normalizeTexturePath(path);
|
||||||
|
return {
|
||||||
|
path: normalizedPath,
|
||||||
|
isVisible: Boolean(
|
||||||
|
normalizedPath && visibleTexturePaths[normalizedPath],
|
||||||
|
),
|
||||||
|
isLoaded: Boolean(
|
||||||
|
normalizedPath &&
|
||||||
|
textureCache[normalizedPath] &&
|
||||||
|
isBrowserTextureSource(textureCache[normalizedPath]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextureSource(path) {
|
||||||
|
const normalizedPath = normalizeTexturePath(path);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBrowserTextureSource(path)) {
|
||||||
|
textureCache[normalizedPath] = String(path).trim();
|
||||||
|
return textureCache[normalizedPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textureCache[normalizedPath] !== undefined) {
|
||||||
|
return textureCache[normalizedPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleTexturePaths[normalizedPath]) {
|
||||||
|
queueTextureRequest(normalizedPath);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StorefrontApp.media = {
|
||||||
|
getTextureState,
|
||||||
|
getTextureSource,
|
||||||
|
scheduleTextureObservation,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -1,5 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
||||||
|
const CATALOG_PAGE_SIZE = 24;
|
||||||
|
|
||||||
function getSelectionKey(state) {
|
function getSelectionKey(state) {
|
||||||
return (
|
return (
|
||||||
@ -56,6 +57,10 @@
|
|||||||
cartOpen: store.getCartOpen(),
|
cartOpen: store.getCartOpen(),
|
||||||
searchQuery: store.getSearchQuery(),
|
searchQuery: store.getSearchQuery(),
|
||||||
cartItems: store.getCartItems(),
|
cartItems: store.getCartItems(),
|
||||||
|
catalogItemsByKey: store.getCatalogItemsByKey(),
|
||||||
|
isCatalogLoading: store.getIsCatalogLoading(),
|
||||||
|
catalogRequestKey: store.getCatalogRequestKey(),
|
||||||
|
catalogPage: store.getCatalogPage(),
|
||||||
isCheckingOut: store.getIsCheckingOut(),
|
isCheckingOut: store.getIsCheckingOut(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -84,11 +89,14 @@
|
|||||||
const queryLabel = state.searchQuery
|
const queryLabel = state.searchQuery
|
||||||
? ` Filtered by "${state.searchQuery}".`
|
? ` Filtered by "${state.searchQuery}".`
|
||||||
: "";
|
: "";
|
||||||
|
const loadingLabel = state.isCatalogLoading
|
||||||
|
? " Pulling live inventory from the game engine."
|
||||||
|
: "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
eyebrow: "Catalog Preview",
|
eyebrow: "Catalog Preview",
|
||||||
title: formatTitle(label),
|
title: formatTitle(label),
|
||||||
copy: `Mock product cards with placeholder imagery sized for future filtering, search, and cart logic.${queryLabel}`,
|
copy: `Live category inventory generated from the game engine for the selected department.${queryLabel}${loadingLabel}`,
|
||||||
badge: "Preview Items",
|
badge: "Preview Items",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -163,18 +171,57 @@
|
|||||||
|
|
||||||
function getVisibleItems(state, catalog) {
|
function getVisibleItems(state, catalog) {
|
||||||
const key = getSelectionKey(state);
|
const key = getSelectionKey(state);
|
||||||
const items = catalog.previewItems[key] || [];
|
const categoryKey = String(key || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const itemsByKey = state.catalogItemsByKey || {};
|
||||||
|
const items = Array.isArray(itemsByKey[categoryKey])
|
||||||
|
? itemsByKey[categoryKey]
|
||||||
|
: [];
|
||||||
|
|
||||||
return items.filter((item) =>
|
return items.filter((item) =>
|
||||||
matchesQuery(state.searchQuery, [
|
matchesQuery(state.searchQuery, [
|
||||||
|
item.className,
|
||||||
item.code,
|
item.code,
|
||||||
item.name,
|
item.name,
|
||||||
item.description,
|
item.description,
|
||||||
item.price,
|
item.price,
|
||||||
|
item.type,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCatalogPagination(state, catalog) {
|
||||||
|
const totalItems = getVisibleItems(state, catalog).length;
|
||||||
|
const totalPages = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(totalItems / CATALOG_PAGE_SIZE),
|
||||||
|
);
|
||||||
|
const currentPage = Math.min(
|
||||||
|
totalPages,
|
||||||
|
Math.max(1, Number(state.catalogPage || 1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageSize: CATALOG_PAGE_SIZE,
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
currentPage,
|
||||||
|
startIndex:
|
||||||
|
totalItems === 0
|
||||||
|
? 0
|
||||||
|
: (currentPage - 1) * CATALOG_PAGE_SIZE + 1,
|
||||||
|
endIndex: Math.min(currentPage * CATALOG_PAGE_SIZE, totalItems),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleItemsPage(state, catalog) {
|
||||||
|
const items = getVisibleItems(state, catalog);
|
||||||
|
const pagination = getCatalogPagination(state, catalog);
|
||||||
|
const startOffset = (pagination.currentPage - 1) * pagination.pageSize;
|
||||||
|
return items.slice(startOffset, startOffset + pagination.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeCart(cartItems) {
|
function summarizeCart(cartItems) {
|
||||||
const itemCount = cartItems.reduce(
|
const itemCount = cartItems.reduce(
|
||||||
(sum, item) => sum + Number(item.quantity || 0),
|
(sum, item) => sum + Number(item.quantity || 0),
|
||||||
@ -205,6 +252,8 @@
|
|||||||
getVisibleCategoryCards,
|
getVisibleCategoryCards,
|
||||||
getVisibleSubcategoryCards,
|
getVisibleSubcategoryCards,
|
||||||
getVisibleItems,
|
getVisibleItems,
|
||||||
|
getVisibleItemsPage,
|
||||||
|
getCatalogPagination,
|
||||||
summarizeCart,
|
summarizeCart,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -102,6 +102,42 @@
|
|||||||
let rootComponent = null;
|
let rootComponent = null;
|
||||||
const injectedStyles = new Set();
|
const injectedStyles = new Set();
|
||||||
|
|
||||||
|
function captureScrollState(container) {
|
||||||
|
if (!container) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
container.querySelectorAll("[data-preserve-scroll-id]"),
|
||||||
|
).map((node) => ({
|
||||||
|
id: node.getAttribute("data-preserve-scroll-id"),
|
||||||
|
scrollTop: node.scrollTop,
|
||||||
|
scrollLeft: node.scrollLeft,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreScrollState(container, entries) {
|
||||||
|
if (!container || !Array.isArray(entries) || entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry || !entry.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = container.querySelector(
|
||||||
|
`[data-preserve-scroll-id="${entry.id}"]`,
|
||||||
|
);
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.scrollTop = Number(entry.scrollTop || 0);
|
||||||
|
target.scrollLeft = Number(entry.scrollLeft || 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function render(component, container) {
|
function render(component, container) {
|
||||||
rootContainer = container;
|
rootContainer = container;
|
||||||
rootComponent = component;
|
rootComponent = component;
|
||||||
@ -113,8 +149,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollState = captureScrollState(rootContainer);
|
||||||
rootContainer.innerHTML = "";
|
rootContainer.innerHTML = "";
|
||||||
rootContainer.appendChild(rootComponent());
|
rootContainer.appendChild(rootComponent());
|
||||||
|
restoreScrollState(rootContainer, scrollState);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureScopedStyle(id, cssText) {
|
function ensureScopedStyle(id, cssText) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user