Merge development into master: shared Web UI runtime, bridge-driven UIs, and server-authoritative store flow #1
140
arma/client/addons/store/ui/_site/components/AppShell.js
Normal file
140
arma/client/addons/store/ui/_site/components/AppShell.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
(function () {
|
||||||
|
const components = (window.StoreComponents = window.StoreComponents || {});
|
||||||
|
|
||||||
|
components.renderAppShell = function renderAppShell(options) {
|
||||||
|
const { header, workspaceNavbar, workspaceBody, cartPanel } = options;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="store-shell">
|
||||||
|
<div class="window-titlebar">
|
||||||
|
<div class="window-titlebar-brand">
|
||||||
|
<span class="window-titlebar-kicker">FORGE Logistics</span>
|
||||||
|
<span class="window-titlebar-title">Supply Exchange</span>
|
||||||
|
</div>
|
||||||
|
<div class="window-titlebar-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-control-btn"
|
||||||
|
disabled
|
||||||
|
title="Minimize unavailable"
|
||||||
|
aria-label="Minimize unavailable"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-control-btn"
|
||||||
|
disabled
|
||||||
|
title="Maximize unavailable"
|
||||||
|
aria-label="Maximize unavailable"
|
||||||
|
>
|
||||||
|
[ ]
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-control-btn is-close"
|
||||||
|
id="store-close-btn"
|
||||||
|
title="Close"
|
||||||
|
aria-label="Close store interface"
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="store-app">
|
||||||
|
<aside class="store-sidebar">
|
||||||
|
<section class="module-card search-module">
|
||||||
|
<div class="module-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Search</span>
|
||||||
|
<h2 class="section-title">Inventory Search</h2>
|
||||||
|
</div>
|
||||||
|
<span class="pill">Module</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search inventory, classes, or suppliers"
|
||||||
|
/>
|
||||||
|
<div class="quick-tags">
|
||||||
|
<span class="quick-tag">Field</span>
|
||||||
|
<span class="quick-tag">Logistics</span>
|
||||||
|
<span class="quick-tag">Issued</span>
|
||||||
|
<span class="quick-tag">Restricted</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module-card">
|
||||||
|
<div class="module-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Filter</span>
|
||||||
|
<h2 class="section-title">Procurement Filters</h2>
|
||||||
|
</div>
|
||||||
|
<span class="pill">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-stack">
|
||||||
|
<div class="filter-group">
|
||||||
|
<span class="filter-label">Department</span>
|
||||||
|
<div class="filter-value">
|
||||||
|
<span>Operational Tier</span>
|
||||||
|
<span class="filter-placeholder">Any</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<span class="filter-label">Availability</span>
|
||||||
|
<div class="filter-value">
|
||||||
|
<span>Stock Window</span>
|
||||||
|
<span class="filter-placeholder">Open</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<span class="filter-label">Approval</span>
|
||||||
|
<div class="filter-value">
|
||||||
|
<span>Purchase Level</span>
|
||||||
|
<span class="filter-placeholder">All</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="store-main">
|
||||||
|
<section class="workspace-card">
|
||||||
|
${workspaceNavbar}
|
||||||
|
<div class="workspace-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">${header.eyebrow}</span>
|
||||||
|
<h1 class="section-title">${header.title}</h1>
|
||||||
|
</div>
|
||||||
|
<span class="pill">${header.badge}</span>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-intro">
|
||||||
|
<p class="section-copy">${header.copy}</p>
|
||||||
|
<span class="inventory-ribbon">${header.ribbon}</span>
|
||||||
|
</div>
|
||||||
|
${workspaceBody}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
${cartPanel}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="store-footer">
|
||||||
|
<div class="footer-block">
|
||||||
|
<span class="footer-title">Procurement Desk</span>
|
||||||
|
<span class="footer-copy">Authorized supply browsing for personnel loadout preparation and mission staging.</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-block">
|
||||||
|
<span class="footer-title">Catalog Scope</span>
|
||||||
|
<span class="footer-copy">Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-block">
|
||||||
|
<span class="footer-title">Module State</span>
|
||||||
|
<span class="footer-copy">Search, filters, and cart presentation are staged now. Purchase logic and item wiring will follow.</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
39
arma/client/addons/store/ui/_site/components/cards.js
Normal file
39
arma/client/addons/store/ui/_site/components/cards.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
(function () {
|
||||||
|
const components = (window.StoreComponents = window.StoreComponents || {});
|
||||||
|
|
||||||
|
components.createCategoryCard = function createCategoryCard(category) {
|
||||||
|
return `
|
||||||
|
<button class="card-button category-card" type="button" data-category="${category.id}">
|
||||||
|
<span class="card-label">${category.label}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
components.createSubCategoryCard = function createSubCategoryCard(
|
||||||
|
category,
|
||||||
|
slotType,
|
||||||
|
) {
|
||||||
|
return `
|
||||||
|
<button class="card-button subcategory-card" type="button" data-subcategory="${category.id}" data-subcategory-type="${slotType}">
|
||||||
|
<span class="card-label">${category.label}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
components.createProductCard = function createProductCard(item) {
|
||||||
|
return `
|
||||||
|
<article class="card-button product-card">
|
||||||
|
<div class="product-image">Image Placeholder</div>
|
||||||
|
<div class="product-meta">
|
||||||
|
<span class="product-code">${item.code}</span>
|
||||||
|
<strong class="product-name">${item.name}</strong>
|
||||||
|
</div>
|
||||||
|
<p class="product-copy">${item.description}</p>
|
||||||
|
<div class="product-footer">
|
||||||
|
<span class="product-price">${item.price}</span>
|
||||||
|
<button class="action-btn" type="button">Add to Cart</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
89
arma/client/addons/store/ui/_site/components/cart-panel.js
Normal file
89
arma/client/addons/store/ui/_site/components/cart-panel.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
(function () {
|
||||||
|
const components = (window.StoreComponents = window.StoreComponents || {});
|
||||||
|
|
||||||
|
components.renderCartPanel = function renderCartPanel(state) {
|
||||||
|
return `
|
||||||
|
<div class="cart-overlay ${state.cartOpen ? "is-open" : ""}" aria-hidden="${state.cartOpen ? "false" : "true"}">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cart-overlay-backdrop"
|
||||||
|
id="store-cart-backdrop"
|
||||||
|
aria-label="Close cart"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<aside class="store-cart-panel">
|
||||||
|
<section class="cart-card">
|
||||||
|
<div class="cart-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Cart</span>
|
||||||
|
<h2 class="section-title">Acquisition Queue</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-control-btn cart-panel-close"
|
||||||
|
id="store-cart-close-btn"
|
||||||
|
aria-label="Close cart"
|
||||||
|
title="Close cart"
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-status">
|
||||||
|
<span class="eyebrow">Status</span>
|
||||||
|
<p class="section-copy">
|
||||||
|
Cart wiring is intentionally deferred. This panel is laid out for order lines, approval totals, and checkout actions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-kpi">
|
||||||
|
<div class="cart-kpi-card">
|
||||||
|
<span class="kpi-label">Lines</span>
|
||||||
|
<span class="kpi-value">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="cart-kpi-card">
|
||||||
|
<span class="kpi-label">Budget</span>
|
||||||
|
<span class="kpi-value">$48,000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-lines">
|
||||||
|
<div class="cart-line">
|
||||||
|
<div class="cart-line-title">Cart Placeholder A</div>
|
||||||
|
<div class="cart-line-meta">Awaiting selection and quantity logic</div>
|
||||||
|
</div>
|
||||||
|
<div class="cart-line">
|
||||||
|
<div class="cart-line-title">Cart Placeholder B</div>
|
||||||
|
<div class="cart-line-meta">Reserved for grouped order summaries</div>
|
||||||
|
</div>
|
||||||
|
<div class="cart-line">
|
||||||
|
<div class="cart-line-title">Cart Placeholder C</div>
|
||||||
|
<div class="cart-line-meta">Reserved for checkout validation status</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Subtotal</span>
|
||||||
|
<span class="summary-value">$0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Fees</span>
|
||||||
|
<span class="summary-value">$0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span class="summary-label">Total</span>
|
||||||
|
<span class="summary-value">$0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-actions">
|
||||||
|
<button type="button" class="action-btn">Review Cart</button>
|
||||||
|
<button type="button" class="action-btn muted-btn">Checkout Locked</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
110
arma/client/addons/store/ui/_site/components/workspace-navbar.js
Normal file
110
arma/client/addons/store/ui/_site/components/workspace-navbar.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
(function () {
|
||||||
|
const components = (window.StoreComponents = window.StoreComponents || {});
|
||||||
|
|
||||||
|
components.getBreadcrumbItems = function getBreadcrumbItems(
|
||||||
|
state,
|
||||||
|
formatTitle,
|
||||||
|
) {
|
||||||
|
const items = [{ id: "categories", label: "Supply Exchange" }];
|
||||||
|
|
||||||
|
if (state.view === "weapons") {
|
||||||
|
items.push({ id: "weapons", label: "Weapons" });
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "vehicles") {
|
||||||
|
items.push({ id: "vehicles", label: "Vehicles" });
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "items") {
|
||||||
|
if (state.selectedWeaponSlot) {
|
||||||
|
items.push({ id: "weapons", label: "Weapons" });
|
||||||
|
items.push({
|
||||||
|
id: "weapon-slot",
|
||||||
|
label: formatTitle(state.selectedWeaponSlot),
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedVehicleSlot) {
|
||||||
|
items.push({ id: "vehicles", label: "Vehicles" });
|
||||||
|
items.push({
|
||||||
|
id: "vehicle-slot",
|
||||||
|
label: formatTitle(state.selectedVehicleSlot),
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedCategory) {
|
||||||
|
items.push({
|
||||||
|
id: "category",
|
||||||
|
label: formatTitle(state.selectedCategory),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
components.renderWorkspaceBreadcrumbs = function renderWorkspaceBreadcrumbs(
|
||||||
|
state,
|
||||||
|
formatTitle,
|
||||||
|
) {
|
||||||
|
const items = components.getBreadcrumbItems(state, formatTitle);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="workspace-breadcrumbs" aria-label="Breadcrumb">
|
||||||
|
${items
|
||||||
|
.map((item, index) => {
|
||||||
|
const isCurrent = index === items.length - 1;
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
return `
|
||||||
|
<span class="breadcrumb-current">${item.label}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="breadcrumb-link"
|
||||||
|
data-breadcrumb-target="${item.id}"
|
||||||
|
>
|
||||||
|
${item.label}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('<span class="breadcrumb-separator">/</span>')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
components.renderWorkspaceCartToggle = function renderWorkspaceCartToggle(
|
||||||
|
state,
|
||||||
|
) {
|
||||||
|
return `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="workspace-cart-btn"
|
||||||
|
id="store-cart-toggle-btn"
|
||||||
|
aria-label="${state.cartOpen ? "Close cart" : "Open cart"}"
|
||||||
|
title="${state.cartOpen ? "Close cart" : "Open cart"}"
|
||||||
|
>
|
||||||
|
<span class="cart-toggle-icon" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
components.renderWorkspaceNavbar = function renderWorkspaceNavbar(
|
||||||
|
state,
|
||||||
|
formatTitle,
|
||||||
|
) {
|
||||||
|
return `
|
||||||
|
<nav class="workspace-navbar" aria-label="Store navigation">
|
||||||
|
${components.renderWorkspaceBreadcrumbs(state, formatTitle)}
|
||||||
|
${components.renderWorkspaceCartToggle(state)}
|
||||||
|
</nav>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
374
arma/client/addons/store/ui/_site/data.js
Normal file
374
arma/client/addons/store/ui/_site/data.js
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
(function () {
|
||||||
|
window.StoreData = {
|
||||||
|
categoryCards: [
|
||||||
|
{ id: "uniforms", label: "Uniforms" },
|
||||||
|
{ id: "headgear", label: "Headgear" },
|
||||||
|
{ id: "facewear", label: "Facewear" },
|
||||||
|
{ id: "vests", label: "Vests" },
|
||||||
|
{ id: "weapons", label: "Weapons" },
|
||||||
|
{ id: "ammo", label: "Ammo" },
|
||||||
|
{ id: "items", label: "Items" },
|
||||||
|
{ id: "vehicles", label: "Vehicles" },
|
||||||
|
],
|
||||||
|
vehicleCards: [
|
||||||
|
{ id: "cars", label: "Cars" },
|
||||||
|
{ id: "armor", label: "Armor" },
|
||||||
|
{ id: "helis", label: "Helicopters" },
|
||||||
|
{ id: "planes", label: "Planes" },
|
||||||
|
{ id: "naval", label: "Naval" },
|
||||||
|
{ id: "other", label: "Other" },
|
||||||
|
],
|
||||||
|
weaponCards: [
|
||||||
|
{ id: "primary", label: "Primary" },
|
||||||
|
{ id: "secondary", label: "Secondary" },
|
||||||
|
{ id: "handgun", label: "Handgun" },
|
||||||
|
],
|
||||||
|
previewItems: {
|
||||||
|
uniforms: [
|
||||||
|
{
|
||||||
|
code: "UNF-102",
|
||||||
|
name: "Field Uniform",
|
||||||
|
description:
|
||||||
|
"Standard issue apparel block reserved for mission-ready clothing sets.",
|
||||||
|
price: "$1,250",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "UNF-214",
|
||||||
|
name: "Combat Uniform",
|
||||||
|
description:
|
||||||
|
"Hardened kit placeholder for armored and specialized duty loadouts.",
|
||||||
|
price: "$1,980",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "UNF-330",
|
||||||
|
name: "Duty Uniform",
|
||||||
|
description:
|
||||||
|
"Administrative and garrison wear preview for storefront layout validation.",
|
||||||
|
price: "$890",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
headgear: [
|
||||||
|
{
|
||||||
|
code: "HDG-044",
|
||||||
|
name: "Patrol Helmet",
|
||||||
|
description:
|
||||||
|
"Protective headgear module with placeholder image frame and pricing slot.",
|
||||||
|
price: "$640",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "HDG-107",
|
||||||
|
name: "Operator Cap",
|
||||||
|
description:
|
||||||
|
"Soft headwear entry for non-armored and low-profile equipment sets.",
|
||||||
|
price: "$120",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "HDG-221",
|
||||||
|
name: "Boonie Hat",
|
||||||
|
description:
|
||||||
|
"Terrain-adapted headwear card for storefront presentation.",
|
||||||
|
price: "$95",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
facewear: [
|
||||||
|
{
|
||||||
|
code: "FAC-015",
|
||||||
|
name: "Protective Goggles",
|
||||||
|
description:
|
||||||
|
"Facewear module placeholder aligned to the shared supply exchange layout.",
|
||||||
|
price: "$220",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "FAC-028",
|
||||||
|
name: "Balaclava",
|
||||||
|
description:
|
||||||
|
"Low-profile face covering preview card for catalog expansion.",
|
||||||
|
price: "$74",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "FAC-091",
|
||||||
|
name: "Respirator Mask",
|
||||||
|
description:
|
||||||
|
"Filtered facewear placeholder for hazard and industrial kit sets.",
|
||||||
|
price: "$410",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vests: [
|
||||||
|
{
|
||||||
|
code: "VST-311",
|
||||||
|
name: "Carrier Rig",
|
||||||
|
description:
|
||||||
|
"Plate carrier preview item with a reserved image zone and pricing footer.",
|
||||||
|
price: "$2,430",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VST-414",
|
||||||
|
name: "Patrol Vest",
|
||||||
|
description:
|
||||||
|
"Mid-weight vest card intended for security and checkpoint loadouts.",
|
||||||
|
price: "$1,320",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VST-558",
|
||||||
|
name: "Utility Harness",
|
||||||
|
description:
|
||||||
|
"Storage-focused chest rig placeholder for non-ballistic kit sets.",
|
||||||
|
price: "$760",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ammo: [
|
||||||
|
{
|
||||||
|
code: "AMM-556",
|
||||||
|
name: "5.56 Cartridge Pack",
|
||||||
|
description:
|
||||||
|
"Grouped ammunition supply card with placeholder product art.",
|
||||||
|
price: "$180",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "AMM-762",
|
||||||
|
name: "7.62 Cartridge Pack",
|
||||||
|
description:
|
||||||
|
"Extended-caliber ammunition block for rifle and marksman loadouts.",
|
||||||
|
price: "$220",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "AMM-9MM",
|
||||||
|
name: "9mm Cartridge Pack",
|
||||||
|
description:
|
||||||
|
"Compact sidearm ammunition placeholder entry for layout review.",
|
||||||
|
price: "$70",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
code: "ITM-004",
|
||||||
|
name: "First Aid Kit",
|
||||||
|
description:
|
||||||
|
"Support item placeholder designed to preview general utility inventory cards.",
|
||||||
|
price: "$65",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "ITM-089",
|
||||||
|
name: "Radio Module",
|
||||||
|
description:
|
||||||
|
"Communications item block with the same product card treatment as all categories.",
|
||||||
|
price: "$330",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "ITM-217",
|
||||||
|
name: "Tool Kit",
|
||||||
|
description:
|
||||||
|
"Repair and engineering support module placeholder for store browsing.",
|
||||||
|
price: "$145",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary: [
|
||||||
|
{
|
||||||
|
code: "WPN-PRI-01",
|
||||||
|
name: "Primary Platform A",
|
||||||
|
description:
|
||||||
|
"Primary weapon slot placeholder card for mock store review.",
|
||||||
|
price: "$3,250",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-PRI-02",
|
||||||
|
name: "Primary Platform B",
|
||||||
|
description:
|
||||||
|
"Alternate long-arm placeholder with image frame and metadata treatment.",
|
||||||
|
price: "$3,980",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-PRI-03",
|
||||||
|
name: "Primary Platform C",
|
||||||
|
description:
|
||||||
|
"General-purpose primary slot preview for future catalog wiring.",
|
||||||
|
price: "$2,890",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
code: "WPN-SEC-01",
|
||||||
|
name: "Secondary Launcher A",
|
||||||
|
description:
|
||||||
|
"Secondary slot placeholder card for support and utility weapon systems.",
|
||||||
|
price: "$5,600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-SEC-02",
|
||||||
|
name: "Secondary Launcher B",
|
||||||
|
description:
|
||||||
|
"Compact shoulder-fired placeholder entry in the shared product style.",
|
||||||
|
price: "$4,950",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-SEC-03",
|
||||||
|
name: "Secondary Launcher C",
|
||||||
|
description:
|
||||||
|
"Reserved card for extended secondary inventory logic tomorrow.",
|
||||||
|
price: "$6,120",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handgun: [
|
||||||
|
{
|
||||||
|
code: "WPN-HND-01",
|
||||||
|
name: "Sidearm A",
|
||||||
|
description:
|
||||||
|
"Handgun slot placeholder card with shared visual language.",
|
||||||
|
price: "$780",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-HND-02",
|
||||||
|
name: "Sidearm B",
|
||||||
|
description:
|
||||||
|
"Secondary sidearm preview block for storefront evaluation.",
|
||||||
|
price: "$920",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "WPN-HND-03",
|
||||||
|
name: "Sidearm C",
|
||||||
|
description:
|
||||||
|
"Compact sidearm placeholder with the same product framing.",
|
||||||
|
price: "$860",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cars: [
|
||||||
|
{
|
||||||
|
code: "VEH-CAR-01",
|
||||||
|
name: "Patrol Utility Car",
|
||||||
|
description:
|
||||||
|
"Light wheeled vehicle placeholder for quick response and urban transport.",
|
||||||
|
price: "$12,500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-CAR-02",
|
||||||
|
name: "Transport Van",
|
||||||
|
description:
|
||||||
|
"Personnel transport preview card using the shared product layout.",
|
||||||
|
price: "$18,200",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-CAR-03",
|
||||||
|
name: "Recon SUV",
|
||||||
|
description:
|
||||||
|
"Recon-focused platform placeholder with pricing and metadata treatment.",
|
||||||
|
price: "$21,900",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
armor: [
|
||||||
|
{
|
||||||
|
code: "VEH-ARM-01",
|
||||||
|
name: "APC Variant A",
|
||||||
|
description:
|
||||||
|
"Armored personnel carrier placeholder reserved for heavy vehicle inventory.",
|
||||||
|
price: "$145,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-ARM-02",
|
||||||
|
name: "IFV Variant B",
|
||||||
|
description:
|
||||||
|
"Tracked armored platform preview aligned to category card behavior.",
|
||||||
|
price: "$228,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-ARM-03",
|
||||||
|
name: "Support Armor C",
|
||||||
|
description:
|
||||||
|
"Heavy support vehicle placeholder for future role-based filtering.",
|
||||||
|
price: "$174,500",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
helis: [
|
||||||
|
{
|
||||||
|
code: "VEH-HEL-01",
|
||||||
|
name: "Light Heli A",
|
||||||
|
description:
|
||||||
|
"Rotorcraft placeholder for scouting and rapid insertion use cases.",
|
||||||
|
price: "$325,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-HEL-02",
|
||||||
|
name: "Transport Heli B",
|
||||||
|
description:
|
||||||
|
"Medium-lift helicopter preview item with staged catalog metadata.",
|
||||||
|
price: "$482,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-HEL-03",
|
||||||
|
name: "Attack Heli C",
|
||||||
|
description:
|
||||||
|
"Combat helicopter placeholder for future weapon package wiring.",
|
||||||
|
price: "$690,000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
planes: [
|
||||||
|
{
|
||||||
|
code: "VEH-PLN-01",
|
||||||
|
name: "Fixed-Wing Trainer",
|
||||||
|
description:
|
||||||
|
"Basic aircraft placeholder for pilot training and logistics transfer.",
|
||||||
|
price: "$760,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-PLN-02",
|
||||||
|
name: "Utility Plane",
|
||||||
|
description:
|
||||||
|
"General-purpose plane preview card in the shared storefront style.",
|
||||||
|
price: "$1,120,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-PLN-03",
|
||||||
|
name: "Strike Plane",
|
||||||
|
description:
|
||||||
|
"Fixed-wing strike platform placeholder for high-tier procurement.",
|
||||||
|
price: "$1,860,000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
naval: [
|
||||||
|
{
|
||||||
|
code: "VEH-NAV-01",
|
||||||
|
name: "Patrol Boat",
|
||||||
|
description:
|
||||||
|
"Shallow-water patrol vessel placeholder for littoral operations.",
|
||||||
|
price: "$92,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-NAV-02",
|
||||||
|
name: "Assault Boat",
|
||||||
|
description:
|
||||||
|
"Assault transport craft preview entry with staged catalog attributes.",
|
||||||
|
price: "$128,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-NAV-03",
|
||||||
|
name: "Support Craft",
|
||||||
|
description:
|
||||||
|
"Utility naval craft placeholder for resupply and extraction workflows.",
|
||||||
|
price: "$104,000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
other: [
|
||||||
|
{
|
||||||
|
code: "VEH-OTH-01",
|
||||||
|
name: "UAV Support Unit",
|
||||||
|
description:
|
||||||
|
"Unmanned vehicle placeholder grouped under miscellaneous platforms.",
|
||||||
|
price: "$48,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-OTH-02",
|
||||||
|
name: "Static Transport Module",
|
||||||
|
description:
|
||||||
|
"Special transport asset preview for non-standard deployment types.",
|
||||||
|
price: "$67,000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "VEH-OTH-03",
|
||||||
|
name: "Service Platform",
|
||||||
|
description:
|
||||||
|
"General support platform placeholder for future specialty categories.",
|
||||||
|
price: "$58,500",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -7,14 +7,26 @@
|
|||||||
<script>
|
<script>
|
||||||
const addonRoot = "forge\\forge_client\\addons\\store\\ui\\_site\\";
|
const addonRoot = "forge\\forge_client\\addons\\store\\ui\\_site\\";
|
||||||
const styleFiles = ["style.css"];
|
const styleFiles = ["style.css"];
|
||||||
const scriptFiles = ["script.js"];
|
const scriptFiles = [
|
||||||
|
"components/AppShell.js",
|
||||||
|
"components/cards.js",
|
||||||
|
"components/cart-panel.js",
|
||||||
|
"components/workspace-navbar.js",
|
||||||
|
"data.js",
|
||||||
|
"logic/state-transitions.js",
|
||||||
|
"logic/workspace.js",
|
||||||
|
"logic/events.js",
|
||||||
|
"script.js",
|
||||||
|
];
|
||||||
|
|
||||||
function requestText(path) {
|
function requestText(path) {
|
||||||
if (
|
if (
|
||||||
typeof A3API !== "undefined" &&
|
typeof A3API !== "undefined" &&
|
||||||
typeof A3API.RequestFile === "function"
|
typeof A3API.RequestFile === "function"
|
||||||
) {
|
) {
|
||||||
return A3API.RequestFile(addonRoot + path);
|
return A3API.RequestFile(
|
||||||
|
addonRoot + path.replace(/\//g, "\\"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(path).then((response) => {
|
return fetch(path).then((response) => {
|
||||||
|
|||||||
77
arma/client/addons/store/ui/_site/logic/events.js
Normal file
77
arma/client/addons/store/ui/_site/logic/events.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
(function () {
|
||||||
|
const logic = (window.StoreLogic = window.StoreLogic || {});
|
||||||
|
|
||||||
|
logic.bindEvents = function bindEvents(options) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
closeStore,
|
||||||
|
renderApp,
|
||||||
|
toggleCart,
|
||||||
|
closeCart,
|
||||||
|
navigateToBreadcrumb,
|
||||||
|
selectCategory,
|
||||||
|
selectSubcategory,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const closeBtn = document.getElementById("store-close-btn");
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener("click", closeStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartToggleBtn = document.getElementById("store-cart-toggle-btn");
|
||||||
|
if (cartToggleBtn) {
|
||||||
|
cartToggleBtn.addEventListener("click", () => {
|
||||||
|
toggleCart(state);
|
||||||
|
renderApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartCloseBtn = document.getElementById("store-cart-close-btn");
|
||||||
|
if (cartCloseBtn) {
|
||||||
|
cartCloseBtn.addEventListener("click", () => {
|
||||||
|
if (closeCart(state)) {
|
||||||
|
renderApp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartBackdrop = document.getElementById("store-cart-backdrop");
|
||||||
|
if (cartBackdrop) {
|
||||||
|
cartBackdrop.addEventListener("click", () => {
|
||||||
|
if (closeCart(state)) {
|
||||||
|
renderApp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-breadcrumb-target]")
|
||||||
|
.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const target = button.getAttribute(
|
||||||
|
"data-breadcrumb-target",
|
||||||
|
);
|
||||||
|
if (navigateToBreadcrumb(state, target)) {
|
||||||
|
renderApp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-category]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const category = button.getAttribute("data-category");
|
||||||
|
selectCategory(state, category);
|
||||||
|
renderApp();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-subcategory]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const subcategory = button.getAttribute("data-subcategory");
|
||||||
|
const slotType = button.getAttribute("data-subcategory-type");
|
||||||
|
selectSubcategory(state, subcategory, slotType);
|
||||||
|
renderApp();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
78
arma/client/addons/store/ui/_site/logic/state-transitions.js
Normal file
78
arma/client/addons/store/ui/_site/logic/state-transitions.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
(function () {
|
||||||
|
const logic = (window.StoreLogic = window.StoreLogic || {});
|
||||||
|
|
||||||
|
logic.toggleCart = function toggleCart(state) {
|
||||||
|
state.cartOpen = !state.cartOpen;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.closeCart = function closeCart(state) {
|
||||||
|
if (!state.cartOpen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.cartOpen = false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.navigateToBreadcrumb = function navigateToBreadcrumb(state, target) {
|
||||||
|
switch (target) {
|
||||||
|
case "categories":
|
||||||
|
state.view = "categories";
|
||||||
|
state.selectedCategory = "";
|
||||||
|
state.selectedWeaponSlot = "";
|
||||||
|
state.selectedVehicleSlot = "";
|
||||||
|
return true;
|
||||||
|
case "weapons":
|
||||||
|
state.view = "weapons";
|
||||||
|
state.selectedCategory = "weapons";
|
||||||
|
state.selectedWeaponSlot = "";
|
||||||
|
state.selectedVehicleSlot = "";
|
||||||
|
return true;
|
||||||
|
case "vehicles":
|
||||||
|
state.view = "vehicles";
|
||||||
|
state.selectedCategory = "vehicles";
|
||||||
|
state.selectedVehicleSlot = "";
|
||||||
|
state.selectedWeaponSlot = "";
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.selectCategory = function selectCategory(state, category) {
|
||||||
|
state.selectedCategory = category;
|
||||||
|
state.selectedWeaponSlot = "";
|
||||||
|
state.selectedVehicleSlot = "";
|
||||||
|
|
||||||
|
if (category === "weapons") {
|
||||||
|
state.view = "weapons";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category === "vehicles") {
|
||||||
|
state.view = "vehicles";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.view = "items";
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.selectSubcategory = function selectSubcategory(
|
||||||
|
state,
|
||||||
|
subcategory,
|
||||||
|
slotType,
|
||||||
|
) {
|
||||||
|
if (slotType === "vehicle") {
|
||||||
|
state.selectedVehicleSlot = subcategory;
|
||||||
|
state.selectedWeaponSlot = "";
|
||||||
|
} else {
|
||||||
|
state.selectedWeaponSlot = subcategory;
|
||||||
|
state.selectedVehicleSlot = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
state.view = "items";
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
})();
|
||||||
113
arma/client/addons/store/ui/_site/logic/workspace.js
Normal file
113
arma/client/addons/store/ui/_site/logic/workspace.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
(function () {
|
||||||
|
const logic = (window.StoreLogic = window.StoreLogic || {});
|
||||||
|
|
||||||
|
logic.formatTitle = function formatTitle(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.getWorkspaceHeader = function getWorkspaceHeader(state, formatTitle) {
|
||||||
|
if (state.view === "weapons") {
|
||||||
|
return {
|
||||||
|
eyebrow: "Weapons Division",
|
||||||
|
title: "Weapon Categories",
|
||||||
|
copy: "Select a weapon slot to open the next supply tier. Primary, secondary, and handgun are scaffolded for tomorrow's item wiring.",
|
||||||
|
badge: "3 Slots",
|
||||||
|
ribbon: "Category Routing Active",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "vehicles") {
|
||||||
|
return {
|
||||||
|
eyebrow: "Vehicle Motorpool",
|
||||||
|
title: "Vehicle Categories",
|
||||||
|
copy: "Select a vehicle class to open the next supply tier. Cars, armor, airframes, and naval options are scaffolded for item wiring.",
|
||||||
|
badge: "6 Classes",
|
||||||
|
ribbon: "Category Routing Active",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "items") {
|
||||||
|
const label =
|
||||||
|
state.selectedWeaponSlot ||
|
||||||
|
state.selectedVehicleSlot ||
|
||||||
|
state.selectedCategory ||
|
||||||
|
"catalog";
|
||||||
|
|
||||||
|
return {
|
||||||
|
eyebrow: "Catalog Preview",
|
||||||
|
title: formatTitle(label),
|
||||||
|
copy: "Mock product cards with placeholder imagery sized for future filtering, search, and cart logic.",
|
||||||
|
badge: "Preview Items",
|
||||||
|
ribbon: "Selection Locked",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eyebrow: "Supply Categories",
|
||||||
|
title: "Procurement Dashboard",
|
||||||
|
copy: "Choose a category to enter the exchange. Weapons and vehicles open a second tier, while the other departments display placeholder product inventory.",
|
||||||
|
badge: "8 Categories",
|
||||||
|
ribbon: "Mock Layout",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
logic.renderWorkspaceBody = function renderWorkspaceBody(
|
||||||
|
state,
|
||||||
|
data,
|
||||||
|
components,
|
||||||
|
) {
|
||||||
|
if (state.view === "weapons") {
|
||||||
|
return `
|
||||||
|
<div class="workspace-grid is-wide">
|
||||||
|
${data.weaponCards
|
||||||
|
.map((category) =>
|
||||||
|
components.createSubCategoryCard(
|
||||||
|
category,
|
||||||
|
"weapon",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "vehicles") {
|
||||||
|
return `
|
||||||
|
<div class="workspace-grid is-wide">
|
||||||
|
${data.vehicleCards
|
||||||
|
.map((category) =>
|
||||||
|
components.createSubCategoryCard(
|
||||||
|
category,
|
||||||
|
"vehicle",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.view === "items") {
|
||||||
|
const key =
|
||||||
|
state.selectedWeaponSlot ||
|
||||||
|
state.selectedVehicleSlot ||
|
||||||
|
state.selectedCategory;
|
||||||
|
const items = data.previewItems[key] || [];
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="workspace-grid is-products">
|
||||||
|
${items.map(components.createProductCard).join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="workspace-grid">
|
||||||
|
${data.categoryCards.map(components.createCategoryCard).join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -3,236 +3,61 @@
|
|||||||
view: "categories",
|
view: "categories",
|
||||||
selectedCategory: "",
|
selectedCategory: "",
|
||||||
selectedWeaponSlot: "",
|
selectedWeaponSlot: "",
|
||||||
|
selectedVehicleSlot: "",
|
||||||
cartOpen: false,
|
cartOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryCards = [
|
function getStoreData(name) {
|
||||||
{ id: "uniforms", label: "Uniforms" },
|
const data = window.StoreData && window.StoreData[name];
|
||||||
{ id: "headgear", label: "Headgear" },
|
if (typeof data === "undefined") {
|
||||||
{ id: "facewear", label: "Facewear" },
|
throw new Error(`[Store UI] Missing store data: ${name}`);
|
||||||
{ id: "vests", label: "Vests" },
|
}
|
||||||
{ id: "weapons", label: "Weapons" },
|
return data;
|
||||||
{ id: "ammo", label: "Ammo" },
|
}
|
||||||
{ id: "items", label: "Items" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const weaponCards = [
|
function getComponentFn(name) {
|
||||||
{ id: "primary", label: "Primary" },
|
const fn = window.StoreComponents && window.StoreComponents[name];
|
||||||
{ id: "secondary", label: "Secondary" },
|
if (typeof fn !== "function") {
|
||||||
{ id: "handgun", label: "Handgun" },
|
throw new Error(`[Store UI] Missing component function: ${name}`);
|
||||||
];
|
}
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
const previewItems = {
|
function getLogicFn(name) {
|
||||||
uniforms: [
|
const fn = window.StoreLogic && window.StoreLogic[name];
|
||||||
{
|
if (typeof fn !== "function") {
|
||||||
code: "UNF-102",
|
throw new Error(`[Store UI] Missing logic function: ${name}`);
|
||||||
name: "Field Uniform",
|
}
|
||||||
description:
|
return fn;
|
||||||
"Standard issue apparel block reserved for mission-ready clothing sets.",
|
}
|
||||||
price: "$1,250",
|
|
||||||
},
|
const data = {
|
||||||
{
|
categoryCards: getStoreData("categoryCards"),
|
||||||
code: "UNF-214",
|
vehicleCards: getStoreData("vehicleCards"),
|
||||||
name: "Combat Uniform",
|
weaponCards: getStoreData("weaponCards"),
|
||||||
description:
|
previewItems: getStoreData("previewItems"),
|
||||||
"Hardened kit placeholder for armored and specialized duty loadouts.",
|
|
||||||
price: "$1,980",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "UNF-330",
|
|
||||||
name: "Duty Uniform",
|
|
||||||
description:
|
|
||||||
"Administrative and garrison wear preview for storefront layout validation.",
|
|
||||||
price: "$890",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
headgear: [
|
|
||||||
{
|
|
||||||
code: "HDG-044",
|
|
||||||
name: "Patrol Helmet",
|
|
||||||
description:
|
|
||||||
"Protective headgear module with placeholder image frame and pricing slot.",
|
|
||||||
price: "$640",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "HDG-107",
|
|
||||||
name: "Operator Cap",
|
|
||||||
description:
|
|
||||||
"Soft headwear entry for non-armored and low-profile equipment sets.",
|
|
||||||
price: "$120",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "HDG-221",
|
|
||||||
name: "Boonie Hat",
|
|
||||||
description:
|
|
||||||
"Terrain-adapted headwear card for storefront presentation.",
|
|
||||||
price: "$95",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facewear: [
|
|
||||||
{
|
|
||||||
code: "FAC-015",
|
|
||||||
name: "Protective Goggles",
|
|
||||||
description:
|
|
||||||
"Facewear module placeholder aligned to the shared supply exchange layout.",
|
|
||||||
price: "$220",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "FAC-028",
|
|
||||||
name: "Balaclava",
|
|
||||||
description:
|
|
||||||
"Low-profile face covering preview card for catalog expansion.",
|
|
||||||
price: "$74",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "FAC-091",
|
|
||||||
name: "Respirator Mask",
|
|
||||||
description:
|
|
||||||
"Filtered facewear placeholder for hazard and industrial kit sets.",
|
|
||||||
price: "$410",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vests: [
|
|
||||||
{
|
|
||||||
code: "VST-311",
|
|
||||||
name: "Carrier Rig",
|
|
||||||
description:
|
|
||||||
"Plate carrier preview item with a reserved image zone and pricing footer.",
|
|
||||||
price: "$2,430",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "VST-414",
|
|
||||||
name: "Patrol Vest",
|
|
||||||
description:
|
|
||||||
"Mid-weight vest card intended for security and checkpoint loadouts.",
|
|
||||||
price: "$1,320",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "VST-558",
|
|
||||||
name: "Utility Harness",
|
|
||||||
description:
|
|
||||||
"Storage-focused chest rig placeholder for non-ballistic kit sets.",
|
|
||||||
price: "$760",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
ammo: [
|
|
||||||
{
|
|
||||||
code: "AMM-556",
|
|
||||||
name: "5.56 Cartridge Pack",
|
|
||||||
description:
|
|
||||||
"Grouped ammunition supply card with placeholder product art.",
|
|
||||||
price: "$180",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "AMM-762",
|
|
||||||
name: "7.62 Cartridge Pack",
|
|
||||||
description:
|
|
||||||
"Extended-caliber ammunition block for rifle and marksman loadouts.",
|
|
||||||
price: "$220",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "AMM-9MM",
|
|
||||||
name: "9mm Cartridge Pack",
|
|
||||||
description:
|
|
||||||
"Compact sidearm ammunition placeholder entry for layout review.",
|
|
||||||
price: "$70",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
code: "ITM-004",
|
|
||||||
name: "First Aid Kit",
|
|
||||||
description:
|
|
||||||
"Support item placeholder designed to preview general utility inventory cards.",
|
|
||||||
price: "$65",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "ITM-089",
|
|
||||||
name: "Radio Module",
|
|
||||||
description:
|
|
||||||
"Communications item block with the same product card treatment as all categories.",
|
|
||||||
price: "$330",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "ITM-217",
|
|
||||||
name: "Tool Kit",
|
|
||||||
description:
|
|
||||||
"Repair and engineering support module placeholder for store browsing.",
|
|
||||||
price: "$145",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
primary: [
|
|
||||||
{
|
|
||||||
code: "WPN-PRI-01",
|
|
||||||
name: "Primary Platform A",
|
|
||||||
description:
|
|
||||||
"Primary weapon slot placeholder card for mock store review.",
|
|
||||||
price: "$3,250",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-PRI-02",
|
|
||||||
name: "Primary Platform B",
|
|
||||||
description:
|
|
||||||
"Alternate long-arm placeholder with image frame and metadata treatment.",
|
|
||||||
price: "$3,980",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-PRI-03",
|
|
||||||
name: "Primary Platform C",
|
|
||||||
description:
|
|
||||||
"General-purpose primary slot preview for future catalog wiring.",
|
|
||||||
price: "$2,890",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
secondary: [
|
|
||||||
{
|
|
||||||
code: "WPN-SEC-01",
|
|
||||||
name: "Secondary Launcher A",
|
|
||||||
description:
|
|
||||||
"Secondary slot placeholder card for support and utility weapon systems.",
|
|
||||||
price: "$5,600",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-SEC-02",
|
|
||||||
name: "Secondary Launcher B",
|
|
||||||
description:
|
|
||||||
"Compact shoulder-fired placeholder entry in the shared product style.",
|
|
||||||
price: "$4,950",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-SEC-03",
|
|
||||||
name: "Secondary Launcher C",
|
|
||||||
description:
|
|
||||||
"Reserved card for extended secondary inventory logic tomorrow.",
|
|
||||||
price: "$6,120",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handgun: [
|
|
||||||
{
|
|
||||||
code: "WPN-HND-01",
|
|
||||||
name: "Sidearm A",
|
|
||||||
description:
|
|
||||||
"Handgun slot placeholder card with shared visual language.",
|
|
||||||
price: "$780",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-HND-02",
|
|
||||||
name: "Sidearm B",
|
|
||||||
description:
|
|
||||||
"Secondary sidearm preview block for storefront evaluation.",
|
|
||||||
price: "$920",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "WPN-HND-03",
|
|
||||||
name: "Sidearm C",
|
|
||||||
description:
|
|
||||||
"Compact sidearm placeholder with the same product framing.",
|
|
||||||
price: "$860",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendEvent(event, data) {
|
const components = {
|
||||||
|
renderAppShell: getComponentFn("renderAppShell"),
|
||||||
|
createCategoryCard: getComponentFn("createCategoryCard"),
|
||||||
|
createSubCategoryCard: getComponentFn("createSubCategoryCard"),
|
||||||
|
createProductCard: getComponentFn("createProductCard"),
|
||||||
|
renderCartPanel: getComponentFn("renderCartPanel"),
|
||||||
|
renderWorkspaceNavbar: getComponentFn("renderWorkspaceNavbar"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTitle = getLogicFn("formatTitle");
|
||||||
|
const getWorkspaceHeader = getLogicFn("getWorkspaceHeader");
|
||||||
|
const renderWorkspaceBody = getLogicFn("renderWorkspaceBody");
|
||||||
|
const bindEvents = getLogicFn("bindEvents");
|
||||||
|
const toggleCart = getLogicFn("toggleCart");
|
||||||
|
const closeCart = getLogicFn("closeCart");
|
||||||
|
const navigateToBreadcrumb = getLogicFn("navigateToBreadcrumb");
|
||||||
|
const selectCategory = getLogicFn("selectCategory");
|
||||||
|
const selectSubcategory = getLogicFn("selectSubcategory");
|
||||||
|
|
||||||
|
function sendEvent(event, payload) {
|
||||||
if (
|
if (
|
||||||
typeof A3API !== "undefined" &&
|
typeof A3API !== "undefined" &&
|
||||||
typeof A3API.SendAlert === "function"
|
typeof A3API.SendAlert === "function"
|
||||||
@ -240,513 +65,44 @@
|
|||||||
A3API.SendAlert(
|
A3API.SendAlert(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
event,
|
event,
|
||||||
data,
|
data: payload,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Store UI]", event, data);
|
console.log("[Store UI]", event, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeStore() {
|
function closeStore() {
|
||||||
sendEvent("store::close", {});
|
sendEvent("store::close", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCart() {
|
|
||||||
state.cartOpen = !state.cartOpen;
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCart() {
|
|
||||||
if (!state.cartOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.cartOpen = false;
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCategoryCard(category) {
|
|
||||||
return `
|
|
||||||
<button class="card-button category-card" type="button" data-category="${category.id}">
|
|
||||||
<span class="card-label">${category.label}</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWeaponCard(category) {
|
|
||||||
return `
|
|
||||||
<button class="card-button subcategory-card" type="button" data-weapon-slot="${category.id}">
|
|
||||||
<span class="card-label">${category.label}</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createProductCard(item) {
|
|
||||||
return `
|
|
||||||
<article class="card-button product-card">
|
|
||||||
<div class="product-image">Image Placeholder</div>
|
|
||||||
<div class="product-meta">
|
|
||||||
<span class="product-code">${item.code}</span>
|
|
||||||
<strong class="product-name">${item.name}</strong>
|
|
||||||
</div>
|
|
||||||
<p class="product-copy">${item.description}</p>
|
|
||||||
<div class="product-footer">
|
|
||||||
<span class="product-price">${item.price}</span>
|
|
||||||
<button class="action-btn" type="button">Add to Cart</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceHeader() {
|
|
||||||
if (state.view === "weapons") {
|
|
||||||
return {
|
|
||||||
eyebrow: "Weapons Division",
|
|
||||||
title: "Weapon Categories",
|
|
||||||
copy: "Select a weapon slot to open the next supply tier. Primary, secondary, and handgun are scaffolded for tomorrow's item wiring.",
|
|
||||||
badge: "3 Slots",
|
|
||||||
ribbon: "Category Routing Active",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.view === "items") {
|
|
||||||
const label =
|
|
||||||
state.selectedWeaponSlot || state.selectedCategory || "catalog";
|
|
||||||
return {
|
|
||||||
eyebrow: "Catalog Preview",
|
|
||||||
title: formatTitle(label),
|
|
||||||
copy: "Mock product cards with placeholder imagery sized for future filtering, search, and cart logic.",
|
|
||||||
badge: "Preview Items",
|
|
||||||
ribbon: "Selection Locked",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
eyebrow: "Supply Categories",
|
|
||||||
title: "Procurement Dashboard",
|
|
||||||
copy: "Choose a category to enter the exchange. Weapons opens a second tier, while the other departments display placeholder product inventory.",
|
|
||||||
badge: "7 Categories",
|
|
||||||
ribbon: "Mock Layout",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTitle(value) {
|
|
||||||
return String(value || "")
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWorkspaceBody() {
|
|
||||||
if (state.view === "weapons") {
|
|
||||||
return `
|
|
||||||
<div class="workspace-grid is-wide">
|
|
||||||
${weaponCards.map(createWeaponCard).join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.view === "items") {
|
|
||||||
const key = state.selectedWeaponSlot || state.selectedCategory;
|
|
||||||
const items = previewItems[key] || [];
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="workspace-grid is-products">
|
|
||||||
${items.map(createProductCard).join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="workspace-grid">
|
|
||||||
${categoryCards.map(createCategoryCard).join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCartPanel() {
|
|
||||||
return `
|
|
||||||
<div class="cart-overlay ${state.cartOpen ? "is-open" : ""}" aria-hidden="${state.cartOpen ? "false" : "true"}">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="cart-overlay-backdrop"
|
|
||||||
id="store-cart-backdrop"
|
|
||||||
aria-label="Close cart"
|
|
||||||
></button>
|
|
||||||
|
|
||||||
<aside class="store-cart-panel">
|
|
||||||
<section class="cart-card">
|
|
||||||
<div class="cart-header">
|
|
||||||
<div>
|
|
||||||
<span class="eyebrow">Cart</span>
|
|
||||||
<h2 class="section-title">Acquisition Queue</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="window-control-btn cart-panel-close"
|
|
||||||
id="store-cart-close-btn"
|
|
||||||
aria-label="Close cart"
|
|
||||||
title="Close cart"
|
|
||||||
>
|
|
||||||
X
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-status">
|
|
||||||
<span class="eyebrow">Status</span>
|
|
||||||
<p class="section-copy">
|
|
||||||
Cart wiring is intentionally deferred. This panel is laid out for order lines, approval totals, and checkout actions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-kpi">
|
|
||||||
<div class="cart-kpi-card">
|
|
||||||
<span class="kpi-label">Lines</span>
|
|
||||||
<span class="kpi-value">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="cart-kpi-card">
|
|
||||||
<span class="kpi-label">Budget</span>
|
|
||||||
<span class="kpi-value">$48,000</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-lines">
|
|
||||||
<div class="cart-line">
|
|
||||||
<div class="cart-line-title">Cart Placeholder A</div>
|
|
||||||
<div class="cart-line-meta">Awaiting selection and quantity logic</div>
|
|
||||||
</div>
|
|
||||||
<div class="cart-line">
|
|
||||||
<div class="cart-line-title">Cart Placeholder B</div>
|
|
||||||
<div class="cart-line-meta">Reserved for grouped order summaries</div>
|
|
||||||
</div>
|
|
||||||
<div class="cart-line">
|
|
||||||
<div class="cart-line-title">Cart Placeholder C</div>
|
|
||||||
<div class="cart-line-meta">Reserved for checkout validation status</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-summary">
|
|
||||||
<div class="summary-row">
|
|
||||||
<span class="summary-label">Subtotal</span>
|
|
||||||
<span class="summary-value">$0</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-row">
|
|
||||||
<span class="summary-label">Fees</span>
|
|
||||||
<span class="summary-value">$0</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-row total">
|
|
||||||
<span class="summary-label">Total</span>
|
|
||||||
<span class="summary-value">$0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="summary-actions">
|
|
||||||
<button type="button" class="action-btn">Review Cart</button>
|
|
||||||
<button type="button" class="action-btn muted-btn">Checkout Locked</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBreadcrumbItems() {
|
|
||||||
const items = [{ id: "categories", label: "Supply Exchange" }];
|
|
||||||
|
|
||||||
if (state.view === "weapons") {
|
|
||||||
items.push({ id: "weapons", label: "Weapons" });
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.view === "items") {
|
|
||||||
if (state.selectedWeaponSlot) {
|
|
||||||
items.push({ id: "weapons", label: "Weapons" });
|
|
||||||
items.push({
|
|
||||||
id: "weapon-slot",
|
|
||||||
label: formatTitle(state.selectedWeaponSlot),
|
|
||||||
});
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.selectedCategory) {
|
|
||||||
items.push({
|
|
||||||
id: "category",
|
|
||||||
label: formatTitle(state.selectedCategory),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWorkspaceBreadcrumbs() {
|
|
||||||
const items = getBreadcrumbItems();
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="workspace-breadcrumbs" aria-label="Breadcrumb">
|
|
||||||
${items
|
|
||||||
.map((item, index) => {
|
|
||||||
const isCurrent = index === items.length - 1;
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
return `
|
|
||||||
<span class="breadcrumb-current">${item.label}</span>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="breadcrumb-link"
|
|
||||||
data-breadcrumb-target="${item.id}"
|
|
||||||
>
|
|
||||||
${item.label}
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join('<span class="breadcrumb-separator">/</span>')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWorkspaceCartToggle() {
|
|
||||||
return `
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="workspace-cart-btn"
|
|
||||||
id="store-cart-toggle-btn"
|
|
||||||
aria-label="${state.cartOpen ? "Close cart" : "Open cart"}"
|
|
||||||
title="${state.cartOpen ? "Close cart" : "Open cart"}"
|
|
||||||
>
|
|
||||||
<span class="cart-toggle-icon" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWorkspaceNavbar() {
|
|
||||||
return `
|
|
||||||
<nav class="workspace-navbar" aria-label="Store navigation">
|
|
||||||
${renderWorkspaceBreadcrumbs()}
|
|
||||||
${renderWorkspaceCartToggle()}
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToBreadcrumb(target) {
|
|
||||||
switch (target) {
|
|
||||||
case "categories":
|
|
||||||
state.view = "categories";
|
|
||||||
state.selectedCategory = "";
|
|
||||||
state.selectedWeaponSlot = "";
|
|
||||||
break;
|
|
||||||
case "weapons":
|
|
||||||
state.view = "weapons";
|
|
||||||
state.selectedCategory = "weapons";
|
|
||||||
state.selectedWeaponSlot = "";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderApp() {
|
function renderApp() {
|
||||||
const header = getWorkspaceHeader();
|
const header = getWorkspaceHeader(state, formatTitle);
|
||||||
|
const workspaceNavbar = components.renderWorkspaceNavbar(
|
||||||
document.getElementById("app").innerHTML = `
|
state,
|
||||||
<div class="store-shell">
|
formatTitle,
|
||||||
<div class="window-titlebar">
|
|
||||||
<div class="window-titlebar-brand">
|
|
||||||
<span class="window-titlebar-kicker">FORGE Logistics</span>
|
|
||||||
<span class="window-titlebar-title">Supply Exchange</span>
|
|
||||||
</div>
|
|
||||||
<div class="window-titlebar-controls">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="window-control-btn"
|
|
||||||
disabled
|
|
||||||
title="Minimize unavailable"
|
|
||||||
aria-label="Minimize unavailable"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="window-control-btn"
|
|
||||||
disabled
|
|
||||||
title="Maximize unavailable"
|
|
||||||
aria-label="Maximize unavailable"
|
|
||||||
>
|
|
||||||
[ ]
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="window-control-btn is-close"
|
|
||||||
id="store-close-btn"
|
|
||||||
title="Close"
|
|
||||||
aria-label="Close store interface"
|
|
||||||
>
|
|
||||||
X
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="store-app">
|
|
||||||
<aside class="store-sidebar">
|
|
||||||
<section class="module-card search-module">
|
|
||||||
<div class="module-header">
|
|
||||||
<div>
|
|
||||||
<span class="eyebrow">Search</span>
|
|
||||||
<h2 class="section-title">Inventory Search</h2>
|
|
||||||
</div>
|
|
||||||
<span class="pill">Module</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="Search inventory, classes, or suppliers"
|
|
||||||
/>
|
|
||||||
<div class="quick-tags">
|
|
||||||
<span class="quick-tag">Field</span>
|
|
||||||
<span class="quick-tag">Logistics</span>
|
|
||||||
<span class="quick-tag">Issued</span>
|
|
||||||
<span class="quick-tag">Restricted</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="module-card">
|
|
||||||
<div class="module-header">
|
|
||||||
<div>
|
|
||||||
<span class="eyebrow">Filter</span>
|
|
||||||
<h2 class="section-title">Procurement Filters</h2>
|
|
||||||
</div>
|
|
||||||
<span class="pill">Pending</span>
|
|
||||||
</div>
|
|
||||||
<div class="filter-stack">
|
|
||||||
<div class="filter-group">
|
|
||||||
<span class="filter-label">Department</span>
|
|
||||||
<div class="filter-value">
|
|
||||||
<span>Operational Tier</span>
|
|
||||||
<span class="filter-placeholder">Any</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<span class="filter-label">Availability</span>
|
|
||||||
<div class="filter-value">
|
|
||||||
<span>Stock Window</span>
|
|
||||||
<span class="filter-placeholder">Open</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<span class="filter-label">Approval</span>
|
|
||||||
<div class="filter-value">
|
|
||||||
<span>Purchase Level</span>
|
|
||||||
<span class="filter-placeholder">All</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="store-main">
|
|
||||||
<section class="workspace-card">
|
|
||||||
${renderWorkspaceNavbar()}
|
|
||||||
<div class="workspace-header">
|
|
||||||
<div>
|
|
||||||
<span class="eyebrow">${header.eyebrow}</span>
|
|
||||||
<h1 class="section-title">${header.title}</h1>
|
|
||||||
</div>
|
|
||||||
<span class="pill">${header.badge}</span>
|
|
||||||
</div>
|
|
||||||
<div class="workspace-intro">
|
|
||||||
<p class="section-copy">${header.copy}</p>
|
|
||||||
<span class="inventory-ribbon">${header.ribbon}</span>
|
|
||||||
</div>
|
|
||||||
${renderWorkspaceBody()}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
${renderCartPanel()}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="store-footer">
|
|
||||||
<div class="footer-block">
|
|
||||||
<span class="footer-title">Procurement Desk</span>
|
|
||||||
<span class="footer-copy">Authorized supply browsing for personnel loadout preparation and mission staging.</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-block">
|
|
||||||
<span class="footer-title">Catalog Scope</span>
|
|
||||||
<span class="footer-copy">Uniforms, protective gear, weapon slots, ammunition groups, and general support inventory.</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-block">
|
|
||||||
<span class="footer-title">Module State</span>
|
|
||||||
<span class="footer-copy">Search, filters, and cart presentation are staged now. Purchase logic and item wiring will follow.</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
const closeBtn = document.getElementById("store-close-btn");
|
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.addEventListener("click", closeStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cartToggleBtn = document.getElementById("store-cart-toggle-btn");
|
|
||||||
if (cartToggleBtn) {
|
|
||||||
cartToggleBtn.addEventListener("click", toggleCart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cartCloseBtn = document.getElementById("store-cart-close-btn");
|
|
||||||
if (cartCloseBtn) {
|
|
||||||
cartCloseBtn.addEventListener("click", closeCart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cartBackdrop = document.getElementById("store-cart-backdrop");
|
|
||||||
if (cartBackdrop) {
|
|
||||||
cartBackdrop.addEventListener("click", closeCart);
|
|
||||||
}
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll("[data-breadcrumb-target]")
|
|
||||||
.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
navigateToBreadcrumb(
|
|
||||||
button.getAttribute("data-breadcrumb-target"),
|
|
||||||
);
|
);
|
||||||
});
|
const workspaceBody = renderWorkspaceBody(state, data, components);
|
||||||
|
const cartPanel = components.renderCartPanel(state);
|
||||||
|
|
||||||
|
document.getElementById("app").innerHTML = components.renderAppShell({
|
||||||
|
header,
|
||||||
|
workspaceNavbar,
|
||||||
|
workspaceBody,
|
||||||
|
cartPanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll("[data-category]").forEach((button) => {
|
bindEvents({
|
||||||
button.addEventListener("click", () => {
|
state,
|
||||||
const category = button.getAttribute("data-category");
|
closeStore,
|
||||||
state.selectedCategory = category;
|
renderApp,
|
||||||
state.selectedWeaponSlot = "";
|
toggleCart,
|
||||||
|
closeCart,
|
||||||
if (category === "weapons") {
|
navigateToBreadcrumb,
|
||||||
state.view = "weapons";
|
selectCategory,
|
||||||
} else {
|
selectSubcategory,
|
||||||
state.view = "items";
|
|
||||||
}
|
|
||||||
|
|
||||||
renderApp();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("[data-weapon-slot]").forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
state.selectedWeaponSlot =
|
|
||||||
button.getAttribute("data-weapon-slot");
|
|
||||||
state.view = "items";
|
|
||||||
renderApp();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user