diff --git a/.gitea/CONTRIBUTING.md b/.gitea/CONTRIBUTING.md index b7b4679..a378b46 100644 --- a/.gitea/CONTRIBUTING.md +++ b/.gitea/CONTRIBUTING.md @@ -1,12 +1,17 @@ # Contributing Setup & Guidelines ## Setting up the Development Environment + ### 1. Clone the repository from GitHub + ### 2. Install HEMTT + The latest version of HEMTT can be installed by running: + ```cmd winget install hemtt ``` ## Coding Guidelines + This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines). diff --git a/.gitea/ISSUE_TEMPLATE/bug-report.md b/.gitea/ISSUE_TEMPLATE/bug-report.md index d4c384f..2e818b5 100644 --- a/.gitea/ISSUE_TEMPLATE/bug-report.md +++ b/.gitea/ISSUE_TEMPLATE/bug-report.md @@ -1,25 +1,31 @@ --- name: Bug report about: Create a bug report to help us improve -title: '' +title: "" labels: kind/bug --- ## Describe the bug + A clear and concise description of what the bug is. ## To reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Expected behavior + A clear and concise description of what you expected to happen. ## Attachments + If applicable, add screenshots or RPT logs to help explain your problem. ## Additional context + Add any other context about the problem here. diff --git a/.gitea/ISSUE_TEMPLATE/feature-request.md b/.gitea/ISSUE_TEMPLATE/feature-request.md index 709ee6c..7bd655d 100644 --- a/.gitea/ISSUE_TEMPLATE/feature-request.md +++ b/.gitea/ISSUE_TEMPLATE/feature-request.md @@ -1,15 +1,18 @@ --- name: Feature Request about: Suggest a feature to be added -title: '' +title: "" labels: kind/feature-request --- ## Describe the feature that you would like + A clear and concise description of the feature you'd want. ## Possible alternatives + Possible alternatives to your suggestion. ## Additional context + Add any other context about the feature here. diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md index 6f72c35..1721684 100644 --- a/.gitea/PULL_REQUEST_TEMPLATE.md +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,16 @@ **When merged this pull request will:** + - Describe what this pull request will do - Each change in a separate line ### Important + - [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request. - [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied. - [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`. + ### Known Issues + - [ ] Issue diff --git a/Architecture_Diagram.md b/Architecture_Diagram.md index 08bb9dd..85521aa 100644 --- a/Architecture_Diagram.md +++ b/Architecture_Diagram.md @@ -105,15 +105,18 @@ sequenceDiagram ## 🚀 **Performance Characteristics** ### **Access Times** + - **Hot Cache (Server)**: `< 1ms` (HashMap lookup) - **Cold Storage (Redis)**: `1-5ms` (Network + Redis) - **Client Cache**: `< 0.1ms` (Local object access) ### **Cache Hit Ratios** + - **Hot Cache**: `~95%` (Active players) - **Cold Storage**: `~5%` (New connections, cache misses) ### **Memory Usage** + - **Server Registry**: `~1KB per active player` - **Client Cache**: `~500B per player object` - **Redis**: `~2KB per player (persistent)` diff --git a/LICENSE.md b/LICENSE.md index 659cbdc..0cb1a9b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -6,10 +6,10 @@ PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions: -* **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material). -* **Noncommercial** - You may not use this material for any commercial purposes. -* **Arma Only** - You may not convert or adapt this material to be used in other games than Arma. -* **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license. +- **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material). +- **Noncommercial** - You may not use this material for any commercial purposes. +- **Arma Only** - You may not convert or adapt this material to be used in other games than Arma. +- **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license. --- @@ -97,7 +97,7 @@ For the avoidance of doubt, this Section 4 supplements and does not replace Your 2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. - For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. @@ -116,4 +116,4 @@ For the avoidance of doubt, this Section 4 supplements and does not replace Your ### Bohemia Interactive Notices 1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor". -2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s. \ No newline at end of file +2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s. diff --git a/README.md b/README.md index 7d1392a..c97d5c2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ graph TD ``` **Communication Flow**: + - **Clients** → Use events (`CBA_Events`) to communicate with server - **Server** → Calls Rust extension via `callExtension` - **Extension** → Manages Redis connection pool and data operations @@ -87,12 +88,14 @@ forge/ 1. Clone the repository from Gitea 2. Install HEMTT -The latest version of HEMTT can be installed by running: + The latest version of HEMTT can be installed by running: + ```cmd winget install hemtt ``` ### Coding Guidelines + This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines). ### Building the Extension @@ -143,14 +146,18 @@ private _update = createHashMapFromArray [["bank", 1500]]; ## Core Modules ### Models + Defines strict data structures with built-in validation: + - `Actor`: Player data (stats, inventory, position) - `Org`: Organization/clan data (members, roles, metadata) [Documentation](lib/models/README.md) ### Repositories + Manages data persistence with Redis: + - Hash-based storage for structured data - Set-based storage for collections - Generic over Redis client implementations @@ -158,7 +165,9 @@ Manages data persistence with Redis: [Documentation](lib/repositories/README.md) ### Services + Implements business logic and orchestration: + - Get-or-create patterns - Data validation and transformation - Complex workflows @@ -166,7 +175,9 @@ Implements business logic and orchestration: [Documentation](lib/services/README.md) ### Extension + Arma 3 interface layer: + - Command routing and parsing - Session management - Error handling and logging @@ -174,7 +185,9 @@ Arma 3 interface layer: [Documentation](arma/server/extension/README.md) ### Client Mod + Client-side SQF addon that provides: + - **UI Components**: Player interfaces for inventory, organizations, banking - **Event Handlers**: CBA event listeners for server communication - **Optimistic Caching**: Local data caching for instant UI updates @@ -182,6 +195,7 @@ Client-side SQF addon that provides: - **Input Validation**: Client-side validation before server requests The client mod communicates with the server using **CBA Events**, ensuring: + - No direct extension calls from clients (security) - Event-driven architecture for scalability - Automatic state synchronization across all clients @@ -190,28 +204,32 @@ The client mod communicates with the server using **CBA Events**, ensuring: ## Available Commands ### Actor Commands -| Command | Description | -|---------|-------------| -| `actor:get` | Retrieve actor data by UID | -| `actor:create` | Create a new actor | -| `actor:update` | Update actor fields | -| `actor:exists` | Check if actor exists | -| `actor:delete` | Delete actor data | + +| Command | Description | +| -------------- | -------------------------- | +| `actor:get` | Retrieve actor data by UID | +| `actor:create` | Create a new actor | +| `actor:update` | Update actor fields | +| `actor:exists` | Check if actor exists | +| `actor:delete` | Delete actor data | ### Organization Commands -| Command | Description | -|---------|-------------| -| `org:get` | Retrieve organization data | -| `org:create` | Create a new organization | -| `org:update` | Update organization fields | -| `org:exists` | Check if organization exists | -| `org:delete` | Delete organization | -| `org:add_member` | Add member to organization | + +| Command | Description | +| ------------------- | ------------------------------- | +| `org:get` | Retrieve organization data | +| `org:create` | Create a new organization | +| `org:update` | Update organization fields | +| `org:exists` | Check if organization exists | +| `org:delete` | Delete organization | +| `org:add_member` | Add member to organization | | `org:remove_member` | Remove member from organization | -| `org:get_members` | Get all organization members | +| `org:get_members` | Get all organization members | ### Redis Operations + Direct Redis operations for advanced use cases: + - **Common**: Key-value operations (set, get, incr, decr, del) - **Hash**: Structured data (hset, hget, hgetall, hdel) - **List**: Ordered collections (lpush, rpush, lrange, lpop, rpop) @@ -264,6 +282,7 @@ if (_response find "Error:" == 0) then { ## Logging Logs are automatically created in `@forge_server/logs/`: + - `actor.log` - Actor operations - `org.log` - Organization operations - `redis.log` - Redis connection and operations diff --git a/arma/client/.github/CONTRIBUTING.md b/arma/client/.github/CONTRIBUTING.md index b7b4679..a378b46 100644 --- a/arma/client/.github/CONTRIBUTING.md +++ b/arma/client/.github/CONTRIBUTING.md @@ -1,12 +1,17 @@ # Contributing Setup & Guidelines ## Setting up the Development Environment + ### 1. Clone the repository from GitHub + ### 2. Install HEMTT + The latest version of HEMTT can be installed by running: + ```cmd winget install hemtt ``` ## Coding Guidelines + This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines). diff --git a/arma/client/.github/ISSUE_TEMPLATE/bug-report.md b/arma/client/.github/ISSUE_TEMPLATE/bug-report.md index d4c384f..2e818b5 100644 --- a/arma/client/.github/ISSUE_TEMPLATE/bug-report.md +++ b/arma/client/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,25 +1,31 @@ --- name: Bug report about: Create a bug report to help us improve -title: '' +title: "" labels: kind/bug --- ## Describe the bug + A clear and concise description of what the bug is. ## To reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Expected behavior + A clear and concise description of what you expected to happen. ## Attachments + If applicable, add screenshots or RPT logs to help explain your problem. ## Additional context + Add any other context about the problem here. diff --git a/arma/client/.github/ISSUE_TEMPLATE/feature-request.md b/arma/client/.github/ISSUE_TEMPLATE/feature-request.md index 709ee6c..7bd655d 100644 --- a/arma/client/.github/ISSUE_TEMPLATE/feature-request.md +++ b/arma/client/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,15 +1,18 @@ --- name: Feature Request about: Suggest a feature to be added -title: '' +title: "" labels: kind/feature-request --- ## Describe the feature that you would like + A clear and concise description of the feature you'd want. ## Possible alternatives + Possible alternatives to your suggestion. ## Additional context + Add any other context about the feature here. diff --git a/arma/client/.github/PULL_REQUEST_TEMPLATE.md b/arma/client/.github/PULL_REQUEST_TEMPLATE.md index 6f72c35..1721684 100644 --- a/arma/client/.github/PULL_REQUEST_TEMPLATE.md +++ b/arma/client/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,16 @@ **When merged this pull request will:** + - Describe what this pull request will do - Each change in a separate line ### Important + - [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request. - [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied. - [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`. + ### Known Issues + - [ ] Issue diff --git a/arma/client/.github/workflows/check.yml b/arma/client/.github/workflows/check.yml index 9d2f654..abb328f 100644 --- a/arma/client/.github/workflows/check.yml +++ b/arma/client/.github/workflows/check.yml @@ -12,17 +12,17 @@ jobs: validate: runs-on: ubuntu-latest steps: - - name: Checkout the source code - uses: actions/checkout@v4 + - name: Checkout the source code + uses: actions/checkout@v4 - - name: Validate Config - run: python tools/config_style_checker.py - - name: Check for BOM - uses: arma-actions/bom-check@master - with: - path: "addons" + - name: Validate Config + run: python tools/config_style_checker.py + - name: Check for BOM + uses: arma-actions/bom-check@master + with: + path: "addons" - - name: Setup HEMTT - uses: arma-actions/hemtt@v1 - - name: Run HEMTT check - run: hemtt check --pedantic + - name: Setup HEMTT + uses: arma-actions/hemtt@v1 + - name: Run HEMTT check + run: hemtt check --pedantic diff --git a/arma/client/LICENSE.md b/arma/client/LICENSE.md index 659cbdc..0cb1a9b 100644 --- a/arma/client/LICENSE.md +++ b/arma/client/LICENSE.md @@ -6,10 +6,10 @@ PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions: -* **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material). -* **Noncommercial** - You may not use this material for any commercial purposes. -* **Arma Only** - You may not convert or adapt this material to be used in other games than Arma. -* **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license. +- **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material). +- **Noncommercial** - You may not use this material for any commercial purposes. +- **Arma Only** - You may not convert or adapt this material to be used in other games than Arma. +- **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license. --- @@ -97,7 +97,7 @@ For the avoidance of doubt, this Section 4 supplements and does not replace Your 2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. - For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. @@ -116,4 +116,4 @@ For the avoidance of doubt, this Section 4 supplements and does not replace Your ### Bohemia Interactive Notices 1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor". -2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s. \ No newline at end of file +2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s. diff --git a/arma/client/README.md b/arma/client/README.md index f65d2bd..7d8932d 100644 --- a/arma/client/README.md +++ b/arma/client/README.md @@ -18,10 +18,13 @@ The project is entirely **open-source** and any contributions are welcome. ## Core Features + - Feature ## Contributing + For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). ## License + Forge Client is licensed under [APL-SA](./LICENSE.md). diff --git a/arma/client/addons/actor/README.md b/arma/client/addons/actor/README.md index 9dcb48a..e83012b 100644 --- a/arma/client/addons/actor/README.md +++ b/arma/client/addons/actor/README.md @@ -1,4 +1,3 @@ -forge_client_actor -=================== +# forge_client_actor Description for this addon diff --git a/arma/client/addons/actor/ui/_site/index.html b/arma/client/addons/actor/ui/_site/index.html index be117a2..381988c 100644 --- a/arma/client/addons/actor/ui/_site/index.html +++ b/arma/client/addons/actor/ui/_site/index.html @@ -1,39 +1,37 @@ - + - - - - - Interaction Menu - - + - - - - -
- - + const script = document.createElement("script"); + script.text = js; + document.head.appendChild(script); + }); + + + +
+ + diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js index d3893b6..62fcf3a 100644 --- a/arma/client/addons/actor/ui/_site/script.js +++ b/arma/client/addons/actor/ui/_site/script.js @@ -13,11 +13,11 @@ function h(tag, props = {}, ...children) { if (props) { Object.entries(props).forEach(([key, value]) => { - if (key.startsWith('on') && typeof value === 'function') { + if (key.startsWith("on") && typeof value === "function") { el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === 'className') { + } else if (key === "className") { el.className = value; - } else if (key === 'style' && typeof value === 'object') { + } else if (key === "style" && typeof value === "object") { Object.assign(el.style, value); } else { el.setAttribute(key, value); @@ -25,13 +25,13 @@ function h(tag, props = {}, ...children) { }); } - children.forEach(child => { - if (typeof child === 'string' || typeof child === 'number') { + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { el.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { el.appendChild(child); } else if (Array.isArray(child)) { - child.forEach(c => { + child.forEach((c) => { if (c instanceof Node) el.appendChild(c); }); } @@ -52,7 +52,7 @@ function render(component, container) { function _render() { if (_rootContainer && _rootComponent) { - _rootContainer.innerHTML = ''; + _rootContainer.innerHTML = ""; _rootContainer.appendChild(_rootComponent()); } } @@ -211,7 +211,9 @@ function actorReducer(state = initialState, action) { if (definition) { newMenuItems.push(definition); } else { - console.warn(`No definition found for: ${type} - ${value}`); + console.warn( + `No definition found for: ${type} - ${value}`, + ); } } else { console.warn("Invalid action format:", actionItem); @@ -232,7 +234,10 @@ function actorReducer(state = initialState, action) { case ActionTypes.ADD_ACTION: const definition = state.actionDefinitions[action.payload]; - if (definition && !state.menuItems.find((item) => item.id === definition.id)) { + if ( + definition && + !state.menuItems.find((item) => item.id === definition.id) + ) { return { ...state, menuItems: [...state.menuItems, definition], @@ -243,7 +248,9 @@ function actorReducer(state = initialState, action) { case ActionTypes.REMOVE_ACTION: return { ...state, - menuItems: state.menuItems.filter((item) => item.id !== action.payload), + menuItems: state.menuItems.filter( + (item) => item.id !== action.payload, + ), }; case ActionTypes.CLEAR_ACTIONS: @@ -299,7 +306,8 @@ const selectors = { getAvailableActions: (state) => state.availableActions, getBaseMenuItems: (state) => state.baseMenuItems, getActionDefinitions: (state) => state.actionDefinitions, - getMenuItemById: (state, id) => state.menuItems.find((item) => item.id === id), + getMenuItemById: (state, id) => + state.menuItems.find((item) => item.id === id), getMenuItemsCount: (state) => state.menuItems.length, }; @@ -312,9 +320,11 @@ let tooltipEl = null; function createTooltip() { if (!tooltipEl) { - tooltipEl = h('div', { className: 'radial-tooltip' }, - h('div', { className: 'tooltip-title' }), - h('div', { className: 'tooltip-description' }) + tooltipEl = h( + "div", + { className: "radial-tooltip" }, + h("div", { className: "tooltip-title" }), + h("div", { className: "tooltip-description" }), ); document.body.appendChild(tooltipEl); } @@ -323,16 +333,17 @@ function createTooltip() { function showTooltip(item, x, y) { const tooltip = createTooltip(); - tooltip.querySelector('.tooltip-title').textContent = item.title; - tooltip.querySelector('.tooltip-description').textContent = item.description; + tooltip.querySelector(".tooltip-title").textContent = item.title; + tooltip.querySelector(".tooltip-description").textContent = + item.description; tooltip.style.left = `${x + 15}px`; tooltip.style.top = `${y + 10}px`; - tooltip.classList.add('visible'); + tooltip.classList.add("visible"); } function hideTooltip() { if (tooltipEl) { - tooltipEl.classList.remove('visible'); + tooltipEl.classList.remove("visible"); } } @@ -350,36 +361,42 @@ function RadialItem({ item, index, total, onClick }) { const x = centerX + menuRadius * Math.cos(angle) - itemSize / 2; const y = centerY + menuRadius * Math.sin(angle) - itemSize / 2; - const el = h('div', { - className: 'radial-item', - style: { - left: `${x}px`, - top: `${y}px` + const el = h( + "div", + { + className: "radial-item", + style: { + left: `${x}px`, + top: `${y}px`, + }, + onClick: () => onClick(item), }, - onClick: () => onClick(item) - }, - h('div', { className: 'radial-item-title' }, item.title) + h("div", { className: "radial-item-title" }, item.title), ); // Add tooltip events - el.addEventListener('mouseenter', (e) => showTooltip(item, e.clientX, e.clientY)); - el.addEventListener('mousemove', (e) => { - if (tooltipEl && tooltipEl.classList.contains('visible')) { + el.addEventListener("mouseenter", (e) => + showTooltip(item, e.clientX, e.clientY), + ); + el.addEventListener("mousemove", (e) => { + if (tooltipEl && tooltipEl.classList.contains("visible")) { tooltipEl.style.left = `${e.clientX + 15}px`; tooltipEl.style.top = `${e.clientY + 10}px`; } }); - el.addEventListener('mouseleave', hideTooltip); + el.addEventListener("mouseleave", hideTooltip); return el; } function RadialCenter({ onClose }) { - return h('div', { - className: 'radial-center', - onClick: onClose - }, - h('div', { className: 'center-label' }, 'Close') + return h( + "div", + { + className: "radial-center", + onClick: onClose, + }, + h("div", { className: "center-label" }, "Close"), ); } @@ -393,7 +410,7 @@ function RadialMenu() { event: item.action, data: {}, }; - if (typeof A3API !== 'undefined') { + if (typeof A3API !== "undefined") { A3API.SendAlert(JSON.stringify(alert)); } }; @@ -404,27 +421,31 @@ function RadialMenu() { event: "actor::close::menu", data: {}, }; - if (typeof A3API !== 'undefined') { + if (typeof A3API !== "undefined") { A3API.SendAlert(JSON.stringify(alert)); } }; if (menuItems.length === 0) { - return h('div', { className: 'empty-state' }, - h('p', null, 'No actions available') + return h( + "div", + { className: "empty-state" }, + h("p", null, "No actions available"), ); } - return h('div', { className: 'radial-menu' }, + return h( + "div", + { className: "radial-menu" }, RadialCenter({ onClose: handleClose }), menuItems.map((item, index) => RadialItem({ item, index, total: menuItems.length, - onClick: handleItemClick - }) - ) + onClick: handleItemClick, + }), + ), ); } @@ -460,14 +481,14 @@ function initializeMenu() { return; } - const root = document.getElementById('app'); + const root = document.getElementById("app"); if (root) { render(App, root); initialized = true; console.log("Interaction menu initialized successfully"); // Request initial data from A3API - if (typeof A3API !== 'undefined') { + if (typeof A3API !== "undefined") { const alert = { event: "actor::get::actions", data: {}, diff --git a/arma/client/addons/actor/ui/_site/style.css b/arma/client/addons/actor/ui/_site/style.css index c3b2a15..527bc49 100644 --- a/arma/client/addons/actor/ui/_site/style.css +++ b/arma/client/addons/actor/ui/_site/style.css @@ -10,7 +10,8 @@ --border: #e2e8f0; --radius: 8px; --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-lg: + 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --menu-radius: 160px; --item-size: 80px; } @@ -22,7 +23,11 @@ } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; height: 100vh; width: 100vw; background: var(--bg-app); diff --git a/arma/client/addons/bank/README.md b/arma/client/addons/bank/README.md index 677cfc9..a4b1503 100644 --- a/arma/client/addons/bank/README.md +++ b/arma/client/addons/bank/README.md @@ -1,4 +1,3 @@ -forge_client_bank -=================== +# forge_client_bank Description for this addon diff --git a/arma/client/addons/bank/ui/_site/atm.css b/arma/client/addons/bank/ui/_site/atm.css index 16b2bc1..06e429c 100644 --- a/arma/client/addons/bank/ui/_site/atm.css +++ b/arma/client/addons/bank/ui/_site/atm.css @@ -14,7 +14,11 @@ } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; margin: 0; padding: 0; background: transparent; @@ -182,7 +186,7 @@ button { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } - &+& { + & + & { margin-left: 1rem; } } diff --git a/arma/client/addons/bank/ui/_site/atm.html b/arma/client/addons/bank/ui/_site/atm.html index 0b0e432..19c28f5 100644 --- a/arma/client/addons/bank/ui/_site/atm.html +++ b/arma/client/addons/bank/ui/_site/atm.html @@ -1,41 +1,45 @@ - - - - - ATM - - + - - - - -
- - - + const atm = document.createElement("script"); + atm.text = atmJs; + document.head.appendChild(atm); + }); + + + +
+ + + diff --git a/arma/client/addons/bank/ui/_site/atm.js b/arma/client/addons/bank/ui/_site/atm.js index c6b9eee..b5632dc 100644 --- a/arma/client/addons/bank/ui/_site/atm.js +++ b/arma/client/addons/bank/ui/_site/atm.js @@ -10,24 +10,24 @@ function h(tag, props = {}, ...children) { const el = document.createElement(tag); if (props) { Object.entries(props).forEach(([key, value]) => { - if (key.startsWith('on') && typeof value === 'function') { + if (key.startsWith("on") && typeof value === "function") { el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === 'className') { + } else if (key === "className") { el.className = value; - } else if (key === 'style' && typeof value === 'object') { + } else if (key === "style" && typeof value === "object") { Object.assign(el.style, value); } else { el.setAttribute(key, value); } }); } - children.forEach(child => { - if (typeof child === 'string' || typeof child === 'number') { + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { el.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { el.appendChild(child); } else if (Array.isArray(child)) { - child.forEach(c => { + child.forEach((c) => { if (c instanceof Node) el.appendChild(c); }); } @@ -46,7 +46,7 @@ function render(component, container) { function _render() { if (_rootContainer && _rootComponent) { - _rootContainer.innerHTML = ''; + _rootContainer.innerHTML = ""; _rootContainer.appendChild(_rootComponent()); } } @@ -55,7 +55,7 @@ const createSignal = (initialValue) => { let _val = initialValue; const getValue = () => _val; const setValue = (newValue) => { - _val = typeof newValue === 'function' ? newValue(_val) : newValue; + _val = typeof newValue === "function" ? newValue(_val) : newValue; _render(); }; return [getValue, setValue]; @@ -65,19 +65,21 @@ const createSignal = (initialValue) => { // #region STATE //============================================================================= -const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance' -const [getPin, setPin] = createSignal(''); -const [getCustomAmount, setCustomAmount] = createSignal(''); -const [getMessage, setMessage] = createSignal(''); +const [getView, setView] = createSignal("pin"); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance' +const [getPin, setPin] = createSignal(""); +const [getCustomAmount, setCustomAmount] = createSignal(""); +const [getMessage, setMessage] = createSignal(""); //============================================================================= // #region UI COMPONENTS //============================================================================= function Header() { - return h('div', { className: 'header', style: { marginBottom: '2rem' } }, - h('h1', null, 'ATM TERMINAL'), - h('p', null, 'Global Financial Network') + return h( + "div", + { className: "header", style: { marginBottom: "2rem" } }, + h("h1", null, "ATM TERMINAL"), + h("p", null, "Global Financial Network"), ); } @@ -86,185 +88,342 @@ function PinView() { const handleNumClick = (num) => { if (currentPin.length < 4) { - setPin(prev => prev + num); + setPin((prev) => prev + num); } }; - const handleClear = () => setPin(''); + const handleClear = () => setPin(""); const handleEnter = () => { if (currentPin.length === 4) { - const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' }; + const state = + typeof store !== "undefined" + ? store.getState() + : { pin: "1234" }; if (currentPin === state.pin) { - setView('menu'); + setView("menu"); } else { - setMessage('Incorrect PIN'); - setPin(''); - setTimeout(() => setMessage(''), 2000); + setMessage("Incorrect PIN"); + setPin(""); + setTimeout(() => setMessage(""), 2000); } } else { - setMessage('Invalid PIN Length'); - setTimeout(() => setMessage(''), 2000); + setMessage("Invalid PIN Length"); + setTimeout(() => setMessage(""), 2000); } }; - return h('div', { className: 'card', style: { padding: '3rem 2rem' } }, - h('h2', null, 'Enter Security PIN'), - h('div', { className: 'pin-display' }, - currentPin.replace(/./g, String.fromCharCode(8226)) || '----' + return h( + "div", + { className: "card", style: { padding: "3rem 2rem" } }, + h("h2", null, "Enter Security PIN"), + h( + "div", + { className: "pin-display" }, + currentPin.replace(/./g, String.fromCharCode(8226)) || "----", ), - h('p', { style: { color: '#ef4444', height: '1.5rem', textAlign: 'center' } }, getMessage()), - h('div', { className: 'numpad' }, - ['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num => - h('button', { onClick: () => handleNumClick(num) }, num) + h( + "p", + { + style: { + color: "#ef4444", + height: "1.5rem", + textAlign: "center", + }, + }, + getMessage(), + ), + h( + "div", + { className: "numpad" }, + ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) => + h("button", { onClick: () => handleNumClick(num) }, num), ), - h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'), - h('button', { onClick: () => handleNumClick('0') }, '0'), - h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629)) - ) + h( + "button", + { + style: { background: "#ef4444", color: "white" }, + onClick: handleClear, + }, + "C", + ), + h("button", { onClick: () => handleNumClick("0") }, "0"), + h( + "button", + { + style: { background: "#10b981", color: "white" }, + onClick: handleEnter, + }, + String.fromCharCode(8629), + ), + ), ); } function MenuView() { - return h('div', { className: 'kiosk-content' }, - h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'), - h('div', { className: 'kiosk-menu-stack' }, - h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') }, - 'Withdraw Cash' + return h( + "div", + { className: "kiosk-content" }, + h( + "h2", + { style: { textAlign: "center", marginBottom: "1rem" } }, + "Select Transaction", + ), + h( + "div", + { className: "kiosk-menu-stack" }, + h( + "button", + { className: "kiosk-btn", onClick: () => setView("withdraw") }, + "Withdraw Cash", ), - h('button', { className: 'kiosk-btn', onClick: () => setView('balance') }, - 'Check Balance' + h( + "button", + { className: "kiosk-btn", onClick: () => setView("balance") }, + "Check Balance", ), - h('button', { - className: 'kiosk-btn', - style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' }, - onClick: () => { - setPin(''); - setView('pin'); - sendEvent('atm::close', {}); - } - }, 'Cancel Transaction') - ) + h( + "button", + { + className: "kiosk-btn", + style: { + background: "var(--bg-surface)", + color: "var(--text-main)", + border: "1px solid var(--border)", + }, + onClick: () => { + setPin(""); + setView("pin"); + sendEvent("atm::close", {}); + }, + }, + "Cancel Transaction", + ), + ), ); } function WithdrawView() { - const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; + const state = + typeof store !== "undefined" + ? store.getState() + : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; const handleWithdraw = (amount) => { if (bankBalance >= amount) { - if (typeof store !== 'undefined') { + if (typeof store !== "undefined") { store.dispatch(withdraw(amount)); } - sendEvent('atm::withdraw', { amount }); + sendEvent("atm::withdraw", { amount }); setMessage(`Please take your cash: $${amount.toLocaleString()}`); setTimeout(() => { - setMessage(''); - setView('menu'); + setMessage(""); + setView("menu"); }, 3000); } else { - setMessage('Insufficient Funds'); - setTimeout(() => setMessage(''), 2000); + setMessage("Insufficient Funds"); + setTimeout(() => setMessage(""), 2000); } }; if (getMessage()) { - return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } }, - h('h2', { style: { color: 'var(--primary)' } }, getMessage()) + return h( + "div", + { + className: "card", + style: { padding: "4rem", textAlign: "center" }, + }, + h("h2", { style: { color: "var(--primary)" } }, getMessage()), ); } - return h('div', { className: 'kiosk-content' }, - h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'), - h('div', { className: 'kiosk-grid' }, - h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'), - h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'), - h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'), - h('button', { - className: 'kiosk-btn', - onClick: () => { - setCustomAmount(''); - setView('custom_withdraw'); - } - }, 'Other Amount'), - h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel') - ) + return h( + "div", + { className: "kiosk-content" }, + h( + "h2", + { style: { textAlign: "center", marginBottom: "1rem" } }, + "Select Amount", + ), + h( + "div", + { className: "kiosk-grid" }, + h( + "button", + { className: "kiosk-btn", onClick: () => handleWithdraw(20) }, + "$20", + ), + h( + "button", + { className: "kiosk-btn", onClick: () => handleWithdraw(50) }, + "$50", + ), + h( + "button", + { className: "kiosk-btn", onClick: () => handleWithdraw(100) }, + "$100", + ), + h( + "button", + { + className: "kiosk-btn", + onClick: () => { + setCustomAmount(""); + setView("custom_withdraw"); + }, + }, + "Other Amount", + ), + h( + "button", + { + className: "kiosk-btn", + style: { + gridColumn: "span 2", + background: "var(--text-muted)", + }, + onClick: () => setView("menu"), + }, + "Cancel", + ), + ), ); } function CustomWithdrawView() { const currentAmount = getCustomAmount(); - const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; + const state = + typeof store !== "undefined" + ? store.getState() + : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; const handleNumClick = (num) => { if (currentAmount.length < 5) { - setCustomAmount(prev => prev + num); + setCustomAmount((prev) => prev + num); } }; - const handleClear = () => setCustomAmount(''); + const handleClear = () => setCustomAmount(""); const handleEnter = () => { const amount = parseInt(currentAmount, 10); if (amount > 0) { if (bankBalance >= amount) { - if (typeof store !== 'undefined') { + if (typeof store !== "undefined") { store.dispatch(withdraw(amount)); } - sendEvent('atm::withdraw', { amount }); - setMessage(`Please take your cash: $${amount.toLocaleString()}`); + sendEvent("atm::withdraw", { amount }); + setMessage( + `Please take your cash: $${amount.toLocaleString()}`, + ); setTimeout(() => { - setMessage(''); - setView('menu'); + setMessage(""); + setView("menu"); }, 3000); } else { - setMessage('Insufficient Funds'); - setTimeout(() => setMessage(''), 2000); + setMessage("Insufficient Funds"); + setTimeout(() => setMessage(""), 2000); } } else { - setMessage('Invalid Amount'); - setTimeout(() => setMessage(''), 2000); + setMessage("Invalid Amount"); + setTimeout(() => setMessage(""), 2000); } }; if (getMessage()) { - return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } }, - h('h2', { style: { color: 'var(--primary)' } }, getMessage()) + return h( + "div", + { + className: "card", + style: { padding: "4rem", textAlign: "center" }, + }, + h("h2", { style: { color: "var(--primary)" } }, getMessage()), ); } - return h('div', { className: 'card', style: { padding: '3rem 2rem' } }, - h('h2', null, 'Enter Amount'), - h('div', { className: 'pin-display' }, - currentAmount ? `$${currentAmount}` : '$0' + return h( + "div", + { className: "card", style: { padding: "3rem 2rem" } }, + h("h2", null, "Enter Amount"), + h( + "div", + { className: "pin-display" }, + currentAmount ? `$${currentAmount}` : "$0", ), - h('div', { className: 'numpad' }, - ['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num => - h('button', { onClick: () => handleNumClick(num) }, num) + h( + "div", + { className: "numpad" }, + ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) => + h("button", { onClick: () => handleNumClick(num) }, num), + ), + h( + "button", + { + style: { background: "#ef4444", color: "white" }, + onClick: handleClear, + }, + "C", + ), + h("button", { onClick: () => handleNumClick("0") }, "0"), + h( + "button", + { + style: { background: "#10b981", color: "white" }, + onClick: handleEnter, + }, + String.fromCharCode(8629), ), - h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'), - h('button', { onClick: () => handleNumClick('0') }, '0'), - h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629)) ), - h('button', { - style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' }, - onClick: () => setView('withdraw') - }, 'Cancel') + h( + "button", + { + style: { + width: "100%", + marginTop: "2rem", + padding: "1rem", + background: "var(--text-muted)", + }, + onClick: () => setView("withdraw"), + }, + "Cancel", + ), ); } function BalanceView() { - const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; + const state = + typeof store !== "undefined" + ? store.getState() + : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; - return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } }, - h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'), - h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } }, - '$' + bankBalance.toLocaleString() + return h( + "div", + { className: "card", style: { textAlign: "center", padding: "3rem" } }, + h("h2", { style: { color: "var(--text-muted)" } }, "Available Balance"), + h( + "div", + { + style: { + fontSize: "4rem", + fontWeight: "800", + margin: "2rem 0", + color: "var(--primary-hover)", + }, + }, + "$" + bankBalance.toLocaleString(), + ), + h( + "button", + { + className: "kiosk-btn", + style: { width: "100%", maxWidth: "300px", margin: "0 auto" }, + onClick: () => setView("menu"), + }, + "Return to Menu", ), - h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu') ); } @@ -272,23 +431,22 @@ function App() { const view = getView(); let mainContent; - if (view === 'pin') { + if (view === "pin") { mainContent = PinView(); - } else if (view === 'menu') { + } else if (view === "menu") { mainContent = MenuView(); - } else if (view === 'withdraw') { + } else if (view === "withdraw") { mainContent = WithdrawView(); - } else if (view === 'custom_withdraw') { + } else if (view === "custom_withdraw") { mainContent = CustomWithdrawView(); - } else if (view === 'balance') { + } else if (view === "balance") { mainContent = BalanceView(); } - return h('main', null, - h('div', { className: 'container' }, - Header(), - mainContent - ) + return h( + "main", + null, + h("div", { className: "container" }, Header(), mainContent), ); } @@ -297,10 +455,10 @@ function App() { //============================================================================= function sendEvent(event, data) { - if (typeof A3API !== 'undefined') { + if (typeof A3API !== "undefined") { A3API.SendAlert(JSON.stringify({ event, data })); } else { - console.log('Event:', event, 'Data:', data); + console.log("Event:", event, "Data:", data); } } @@ -313,20 +471,20 @@ let initialized = false; function initATM() { if (initialized) return; - const root = document.getElementById('app'); + const root = document.getElementById("app"); if (root) { - if (typeof store !== 'undefined') { + if (typeof store !== "undefined") { store.subscribe(() => _render()); } render(App, root); initialized = true; - console.log('[ATM] Interface initialized'); + console.log("[ATM] Interface initialized"); } } -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initATM); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initATM); } else { initATM(); } diff --git a/arma/client/addons/bank/ui/_site/bank.css b/arma/client/addons/bank/ui/_site/bank.css index e47ed66..ff6e649 100644 --- a/arma/client/addons/bank/ui/_site/bank.css +++ b/arma/client/addons/bank/ui/_site/bank.css @@ -22,7 +22,11 @@ body { } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; margin: 0; padding: 0; background: var(--bg-app); @@ -198,7 +202,7 @@ main { font-size: 0.9rem; font-weight: 600; color: var(--text-main); - font-family: 'Consolas', 'Monaco', monospace; + font-family: "Consolas", "Monaco", monospace; } .content { @@ -244,7 +248,7 @@ button { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } - &+& { + & + & { margin-left: 1rem; } } diff --git a/arma/client/addons/bank/ui/_site/bank.html b/arma/client/addons/bank/ui/_site/bank.html index 7002265..a2e2b94 100644 --- a/arma/client/addons/bank/ui/_site/bank.html +++ b/arma/client/addons/bank/ui/_site/bank.html @@ -1,47 +1,45 @@ - + - - - - - FDIC - Global Financial Network - - + - - - - -
- - - + const bankScript = document.createElement("script"); + bankScript.text = bankJs; + document.head.appendChild(bankScript); + }); + + + +
+ + + diff --git a/arma/client/addons/bank/ui/_site/bank.js b/arma/client/addons/bank/ui/_site/bank.js index 7b5209b..01f0acc 100644 --- a/arma/client/addons/bank/ui/_site/bank.js +++ b/arma/client/addons/bank/ui/_site/bank.js @@ -10,26 +10,31 @@ function h(tag, props = {}, ...children) { const el = document.createElement(tag); if (props) { Object.entries(props).forEach(([key, value]) => { - if (key.startsWith('on') && typeof value === 'function') { + if (key.startsWith("on") && typeof value === "function") { el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === 'className') { + } else if (key === "className") { el.className = value; - } else if (key === 'style' && typeof value === 'object') { + } else if (key === "style" && typeof value === "object") { Object.assign(el.style, value); - } else if (key === 'disabled' || key === 'checked' || key === 'selected' || key === 'readonly') { + } else if ( + key === "disabled" || + key === "checked" || + key === "selected" || + key === "readonly" + ) { if (value) el[key] = true; } else { el.setAttribute(key, value); } }); } - children.forEach(child => { - if (typeof child === 'string' || typeof child === 'number') { + children.forEach((child) => { + if (typeof child === "string" || typeof child === "number") { el.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { el.appendChild(child); } else if (Array.isArray(child)) { - child.forEach(c => { + child.forEach((c) => { if (c instanceof Node) el.appendChild(c); }); } @@ -48,7 +53,7 @@ function render(component, container) { function _render() { if (_rootContainer && _rootComponent) { - _rootContainer.innerHTML = ''; + _rootContainer.innerHTML = ""; _rootContainer.appendChild(_rootComponent()); } } @@ -59,52 +64,92 @@ function _render() { function Navbar() { const state = store.getState(); - const uid = state.uid || 'Unknown'; + const uid = state.uid || "Unknown"; - return h('nav', { className: 'navbar' }, - h('div', { className: 'navbar-inner' }, - h('div', { className: 'navbar-brand' }, - h('span', { className: 'navbar-title' }, 'FDIC - Global Financial Network') + return h( + "nav", + { className: "navbar" }, + h( + "div", + { className: "navbar-inner" }, + h( + "div", + { className: "navbar-brand" }, + h( + "span", + { className: "navbar-title" }, + "FDIC - Global Financial Network", + ), ), - h('div', { className: 'navbar-profile' }, - h('div', { className: 'profile-info' }, - h('span', { className: 'profile-label' }, 'Account'), - h('span', { className: 'profile-id' }, uid) - ) - ) - ) + h( + "div", + { className: "navbar-profile" }, + h( + "div", + { className: "profile-info" }, + h("span", { className: "profile-label" }, "Account"), + h("span", { className: "profile-id" }, uid), + ), + ), + ), ); } function WindowTitleBar() { - return h('div', { className: 'window-titlebar' }, - h('div', { className: 'window-titlebar-brand' }, - h('span', { className: 'window-titlebar-kicker' }, 'FDIC Workspace'), - h('span', { className: 'window-titlebar-title' }, 'Global Financial Network') + return h( + "div", + { className: "window-titlebar" }, + h( + "div", + { className: "window-titlebar-brand" }, + h( + "span", + { className: "window-titlebar-kicker" }, + "FDIC Workspace", + ), + h( + "span", + { className: "window-titlebar-title" }, + "Global Financial Network", + ), + ), + h( + "div", + { className: "window-titlebar-controls" }, + h( + "button", + { + type: "button", + className: "window-control-btn", + disabled: true, + title: "Minimize unavailable", + "aria-label": "Minimize unavailable", + }, + "-", + ), + h( + "button", + { + type: "button", + className: "window-control-btn", + disabled: true, + title: "Maximize unavailable", + "aria-label": "Maximize unavailable", + }, + "[ ]", + ), + h( + "button", + { + type: "button", + className: "window-control-btn is-close", + onClick: () => sendEvent("bank::close", {}), + title: "Close", + "aria-label": "Close banking interface", + }, + "X", + ), ), - h('div', { className: 'window-titlebar-controls' }, - h('button', { - type: 'button', - className: 'window-control-btn', - disabled: true, - title: 'Minimize unavailable', - 'aria-label': 'Minimize unavailable' - }, '-'), - h('button', { - type: 'button', - className: 'window-control-btn', - disabled: true, - title: 'Maximize unavailable', - 'aria-label': 'Maximize unavailable' - }, '[ ]'), - h('button', { - type: 'button', - className: 'window-control-btn is-close', - onClick: () => sendEvent('bank::close', {}), - title: 'Close', - 'aria-label': 'Close banking interface' - }, 'X') - ) ); } @@ -112,34 +157,77 @@ function TransactionHistory() { const state = store.getState(); const transactions = state.transactions || []; - return h('div', { className: 'card' }, - h('h3', { style: { textAlign: 'left', borderBottom: '1px solid var(--border)', paddingBottom: '1rem', marginBottom: '1rem' } }, 'Recent Transactions'), + return h( + "div", + { className: "card" }, + h( + "h3", + { + style: { + textAlign: "left", + borderBottom: "1px solid var(--border)", + paddingBottom: "1rem", + marginBottom: "1rem", + }, + }, + "Recent Transactions", + ), transactions.length === 0 - ? h('p', { style: { color: 'var(--text-muted)' } }, 'No transactions yet') - : h('ul', { style: { listStyle: 'none', padding: 0, margin: 0 } }, - transactions.slice(0, 10).map(tx => { - const isCredit = tx.type === 'Deposit'; - return h('li', { - style: { - display: 'flex', - justifyContent: 'space-between', - padding: '0.75rem 0', - borderBottom: '1px solid var(--bg-surface-hover)' - } - }, - h('div', { style: { textAlign: 'left' } }, - h('div', { style: { fontWeight: '500' } }, tx.type), - h('div', { style: { fontSize: '0.85rem', color: 'var(--text-muted)' } }, tx.date) - ), - h('div', { - style: { - fontWeight: '700', - color: isCredit ? '#10b981' : '#ef4444' - } - }, (isCredit ? '+' : '-') + '$' + Math.abs(tx.amount).toLocaleString()) - ); - }) - ) + ? h( + "p", + { style: { color: "var(--text-muted)" } }, + "No transactions yet", + ) + : h( + "ul", + { style: { listStyle: "none", padding: 0, margin: 0 } }, + transactions.slice(0, 10).map((tx) => { + const isCredit = tx.type === "Deposit"; + return h( + "li", + { + style: { + display: "flex", + justifyContent: "space-between", + padding: "0.75rem 0", + borderBottom: + "1px solid var(--bg-surface-hover)", + }, + }, + h( + "div", + { style: { textAlign: "left" } }, + h( + "div", + { style: { fontWeight: "500" } }, + tx.type, + ), + h( + "div", + { + style: { + fontSize: "0.85rem", + color: "var(--text-muted)", + }, + }, + tx.date, + ), + ), + h( + "div", + { + style: { + fontWeight: "700", + color: isCredit ? "#10b981" : "#ef4444", + }, + }, + (isCredit ? "+" : "-") + + "$" + + Math.abs(tx.amount).toLocaleString(), + ), + ); + }), + ), ); } @@ -149,26 +237,26 @@ function DepositWithdrawForm() { const cashBalance = state.accounts.cash; const getAmount = () => { - const input = document.getElementById('deposit-withdraw-amount'); + const input = document.getElementById("deposit-withdraw-amount"); return parseFloat(input?.value) || 0; }; const clearInput = () => { - const input = document.getElementById('deposit-withdraw-amount'); - if (input) input.value = ''; + const input = document.getElementById("deposit-withdraw-amount"); + if (input) input.value = ""; }; const handleDeposit = () => { const amount = getAmount(); if (!amount || amount <= 0) { - console.log('Please enter a valid amount'); + console.log("Please enter a valid amount"); return; } if (amount > cashBalance) { - console.log('Insufficient cash'); + console.log("Insufficient cash"); return; } - sendEvent('bank::deposit', { amount }); + sendEvent("bank::deposit", { amount }); store.dispatch(deposit(amount)); clearInput(); }; @@ -176,37 +264,70 @@ function DepositWithdrawForm() { const handleWithdraw = () => { const amount = getAmount(); if (!amount || amount <= 0) { - console.log('Please enter a valid amount'); + console.log("Please enter a valid amount"); return; } if (amount > bankBalance) { - console.log('Insufficient funds'); + console.log("Insufficient funds"); return; } - sendEvent('bank::withdraw', { amount }); + sendEvent("bank::withdraw", { amount }); store.dispatch(withdraw(amount)); clearInput(); }; - return h('div', { className: 'card' }, - h('h2', null, 'Deposit / Withdraw'), - h('div', { className: 'balance-info' }, - h('div', { className: 'balance-info-item' }, - h('span', { className: 'balance-info-label' }, 'Cash'), - h('span', { className: 'balance-info-value cash' }, '$' + cashBalance.toLocaleString()) + return h( + "div", + { className: "card" }, + h("h2", null, "Deposit / Withdraw"), + h( + "div", + { className: "balance-info" }, + h( + "div", + { className: "balance-info-item" }, + h("span", { className: "balance-info-label" }, "Cash"), + h( + "span", + { className: "balance-info-value cash" }, + "$" + cashBalance.toLocaleString(), + ), + ), + h( + "div", + { className: "balance-info-item" }, + h("span", { className: "balance-info-label" }, "Bank"), + h( + "span", + { className: "balance-info-value" }, + "$" + bankBalance.toLocaleString(), + ), + ), + ), + h( + "div", + { className: "deposit-withdraw-form" }, + h("input", { + id: "deposit-withdraw-amount", + type: "number", + placeholder: "Enter amount...", + min: "1", + }), + h( + "div", + { className: "deposit-withdraw-buttons" }, + h( + "button", + { onClick: handleDeposit, disabled: cashBalance <= 0 }, + "Deposit", + ), + h( + "button", + { onClick: handleWithdraw, disabled: bankBalance <= 0 }, + "Withdraw", + ), ), - h('div', { className: 'balance-info-item' }, - h('span', { className: 'balance-info-label' }, 'Bank'), - h('span', { className: 'balance-info-value' }, '$' + bankBalance.toLocaleString()) - ) ), - h('div', { className: 'deposit-withdraw-form' }, - h('input', { id: 'deposit-withdraw-amount', type: 'number', placeholder: 'Enter amount...', min: '1' }), - h('div', { className: 'deposit-withdraw-buttons' }, - h('button', { onClick: handleDeposit, disabled: cashBalance <= 0 }, 'Deposit'), - h('button', { onClick: handleWithdraw, disabled: bankBalance <= 0 }, 'Withdraw') - ) - ) ); } @@ -218,52 +339,70 @@ function TransferForm() { const handleSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); - const amount = parseFloat(formData.get('amount')); - const playerId = formData.get('playerId'); + const amount = parseFloat(formData.get("amount")); + const playerId = formData.get("playerId"); if (!amount || amount <= 0) { - console.log('Please enter a valid amount'); + console.log("Please enter a valid amount"); return; } const currentState = store.getState(); if (!playerId) { - console.log('Please select a recipient'); + console.log("Please select a recipient"); return; } if (amount > currentState.accounts.bank) { - console.log('Insufficient funds'); + console.log("Insufficient funds"); return; } - sendEvent('bank::transfer', { from: 'bank', amount, target: playerId }); - store.dispatch(transfer('bank', amount, 'player')); + sendEvent("bank::transfer", { from: "bank", amount, target: playerId }); + store.dispatch(transfer("bank", amount, "player")); e.target.reset(); }; // Build player options - const playerOptions = [h('option', { value: '', disabled: true, selected: true }, 'Select player...')]; - Object.keys(players).forEach(uid => { + const playerOptions = [ + h( + "option", + { value: "", disabled: true, selected: true }, + "Select player...", + ), + ]; + Object.keys(players).forEach((uid) => { if (uid !== currentUid && players[uid]?.name) { - playerOptions.push(h('option', { value: uid }, players[uid].name)); + playerOptions.push(h("option", { value: uid }, players[uid].name)); } }); - return h('div', { className: 'card' }, - h('h2', null, 'Wire Transfer'), - h('form', { onSubmit: handleSubmit }, - h('div', null, - h('label', null, 'Recipient'), - h('select', { name: 'playerId' }, playerOptions) + return h( + "div", + { className: "card" }, + h("h2", null, "Wire Transfer"), + h( + "form", + { onSubmit: handleSubmit }, + h( + "div", + null, + h("label", null, "Recipient"), + h("select", { name: "playerId" }, playerOptions), ), - h('div', null, - h('label', null, 'Amount'), - h('input', { name: 'amount', type: 'number', placeholder: '0.00' }) + h( + "div", + null, + h("label", null, "Amount"), + h("input", { + name: "amount", + type: "number", + placeholder: "0.00", + }), ), - h('button', { type: 'submit' }, 'Send Funds') - ) + h("button", { type: "submit" }, "Send Funds"), + ), ); } @@ -272,66 +411,127 @@ function BankDashboard() { const bankBalance = state.accounts.bank; const earnings = state.accounts.earnings; - return h('div', { className: 'content' }, - h('div', { className: 'card', style: { gridColumn: 'span 2' } }, - h('h2', { style: { fontSize: '1.2rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' } }, 'Account Balance'), - h('div', { style: { fontSize: '2.8rem', fontWeight: '800', color: 'var(--primary-hover)', margin: '1rem 0' } }, - '$' + bankBalance.toLocaleString() + return h( + "div", + { className: "content" }, + h( + "div", + { className: "card", style: { gridColumn: "span 2" } }, + h( + "h2", + { + style: { + fontSize: "1.2rem", + color: "var(--text-muted)", + textTransform: "uppercase", + letterSpacing: "0.05em", + }, + }, + "Account Balance", ), - h('div', { style: { textAlign: 'center', color: 'var(--text-muted)', fontSize: '1.1rem', marginBottom: '1rem' } }, - 'Pending: ', - h('span', { style: { color: '#fbbf24', fontWeight: 'bold' } }, '$' + earnings.toLocaleString()) + h( + "div", + { + style: { + fontSize: "2.8rem", + fontWeight: "800", + color: "var(--primary-hover)", + margin: "1rem 0", + }, + }, + "$" + bankBalance.toLocaleString(), + ), + h( + "div", + { + style: { + textAlign: "center", + color: "var(--text-muted)", + fontSize: "1.1rem", + marginBottom: "1rem", + }, + }, + "Pending: ", + h( + "span", + { style: { color: "#fbbf24", fontWeight: "bold" } }, + "$" + earnings.toLocaleString(), + ), + ), + h( + "div", + { className: "deposit-earnings-button" }, + h( + "button", + { + onClick: () => { + sendEvent("bank::depositEarnings", { + amount: earnings, + }); + store.dispatch(depositEarnings(earnings)); + }, + disabled: earnings <= 0, + style: { width: "25%" }, + }, + "Deposit Earnings", + ), ), - h('div', { className: 'deposit-earnings-button' }, - h('button', { - onClick: () => { - sendEvent('bank::depositEarnings', { amount: earnings }); - store.dispatch(depositEarnings(earnings)); - }, disabled: earnings <= 0, style: { width: '25%' } - }, 'Deposit Earnings') - ) ), DepositWithdrawForm(), TransferForm(), - h('div', { style: { gridColumn: 'span 2' } }, TransactionHistory()) + h("div", { style: { gridColumn: "span 2" } }, TransactionHistory()), ); } function Footer() { - return h('div', { className: 'footer' }, - h('div', { className: 'wrapper' }, - h('div', null, - h('h3', null, 'Secure Banking'), - h('ul', { style: { listStyleType: 'none', padding: 0 } }, - h('li', null, 'FDIC Insured'), - h('li', null, 'Fraud Protection'), - h('li', null, '24/7 Support'), - h('li', null, 'API Access') - ) + return h( + "div", + { className: "footer" }, + h( + "div", + { className: "wrapper" }, + h( + "div", + null, + h("h3", null, "Secure Banking"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "FDIC Insured"), + h("li", null, "Fraud Protection"), + h("li", null, "24/7 Support"), + h("li", null, "API Access"), + ), ), - h('div', null, - h('h3', null, 'Notices'), - h('ul', { style: { listStyleType: 'none', padding: 0 } }, - h('li', null, 'Terms of Service'), - h('li', null, 'Privacy Policy'), - h('li', null, 'Interest Rates'), - h('li', null, 'Report Fraud') - ) - ) - ) + h( + "div", + null, + h("h3", null, "Notices"), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + h("li", null, "Terms of Service"), + h("li", null, "Privacy Policy"), + h("li", null, "Interest Rates"), + h("li", null, "Report Fraud"), + ), + ), + ), ); } function App() { - return h('div', { className: 'app-shell' }, + return h( + "div", + { className: "app-shell" }, WindowTitleBar(), - h('main', null, + h( + "main", + null, Navbar(), - h('div', { className: 'container' }, - BankDashboard() - ), - Footer() - ) + h("div", { className: "container" }, BankDashboard()), + Footer(), + ), ); } @@ -340,10 +540,10 @@ function App() { //============================================================================= function sendEvent(event, data) { - if (typeof A3API !== 'undefined') { + if (typeof A3API !== "undefined") { A3API.SendAlert(JSON.stringify({ event, data })); } else { - console.log('Event:', event, 'Data:', data); + console.log("Event:", event, "Data:", data); } } @@ -356,20 +556,20 @@ let initialized = false; function initBank() { if (initialized) return; - const root = document.getElementById('app'); + const root = document.getElementById("app"); if (root) { - if (typeof store !== 'undefined') { + if (typeof store !== "undefined") { store.subscribe(() => _render()); } render(App, root); initialized = true; - console.log('[Bank] Interface initialized'); + console.log("[Bank] Interface initialized"); } } -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initBank); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initBank); } else { initBank(); } diff --git a/arma/client/addons/bank/ui/_site/store.js b/arma/client/addons/bank/ui/_site/store.js index 16c2937..45c195f 100644 --- a/arma/client/addons/bank/ui/_site/store.js +++ b/arma/client/addons/bank/ui/_site/store.js @@ -20,13 +20,13 @@ function createStore(reducer) { const dispatch = (action) => { state = reducer(state, action); - listeners.forEach(listener => listener()); + listeners.forEach((listener) => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { - listeners = listeners.filter(l => l !== listener); + listeners = listeners.filter((l) => l !== listener); }; }; @@ -41,27 +41,27 @@ function createStore(reducer) { // ============================================================================ const initialState = { - uid: '', + uid: "", accounts: { bank: 0, cash: 0, earnings: 0, - org: 0 + org: 0, }, - pin: '1234', - transactions: [] + pin: "1234", + transactions: [], }; // ============================================================================ // ACTION TYPES // ============================================================================ -const DEPOSIT = 'DEPOSIT'; -const DEPOSIT_EARNINGS = 'DEPOSIT_EARNINGS'; -const WITHDRAW = 'WITHDRAW'; -const TRANSFER = 'TRANSFER'; -const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS'; -const UPDATE_PIN = 'UPDATE_PIN'; +const DEPOSIT = "DEPOSIT"; +const DEPOSIT_EARNINGS = "DEPOSIT_EARNINGS"; +const WITHDRAW = "WITHDRAW"; +const TRANSFER = "TRANSFER"; +const UPDATE_ACCOUNTS = "UPDATE_ACCOUNTS"; +const UPDATE_PIN = "UPDATE_PIN"; // ============================================================================ // ACTION CREATORS @@ -69,34 +69,34 @@ const UPDATE_PIN = 'UPDATE_PIN'; const deposit = (amount) => ({ type: DEPOSIT, - payload: amount + payload: amount, }); const depositEarnings = (amount) => ({ type: DEPOSIT_EARNINGS, - payload: amount + payload: amount, }); const withdraw = (amount) => ({ type: WITHDRAW, - payload: amount + payload: amount, }); const transfer = (from, amount, target) => ({ type: TRANSFER, from: from, payload: amount, - target: target + target: target, }); const updateAccounts = (accounts) => ({ type: UPDATE_ACCOUNTS, - payload: accounts + payload: accounts, }); const updatePin = (pin) => ({ type: UPDATE_PIN, - payload: pin + payload: pin, }); // ============================================================================ @@ -107,7 +107,7 @@ function appReducer(state = initialState, action) { switch (action.type) { case DEPOSIT: if (state.accounts.cash < action.payload) { - console.warn('Insufficient cash!'); + console.warn("Insufficient cash!"); return state; } return { @@ -115,21 +115,21 @@ function appReducer(state = initialState, action) { accounts: { ...state.accounts, bank: state.accounts.bank + action.payload, - cash: state.accounts.cash - action.payload + cash: state.accounts.cash - action.payload, }, transactions: [ ...state.transactions, { - type: 'Deposit', + type: "Deposit", amount: action.payload, - date: new Date().toLocaleString() - } - ] + date: new Date().toLocaleString(), + }, + ], }; case DEPOSIT_EARNINGS: if (state.accounts.earnings < action.payload) { - console.warn('Insufficient earnings!'); + console.warn("Insufficient earnings!"); return state; } return { @@ -137,21 +137,21 @@ function appReducer(state = initialState, action) { accounts: { ...state.accounts, bank: state.accounts.bank + action.payload, - earnings: state.accounts.earnings - action.payload + earnings: state.accounts.earnings - action.payload, }, transactions: [ ...state.transactions, { - type: 'Deposit Earnings', + type: "Deposit Earnings", amount: action.payload, - date: new Date().toLocaleString() - } - ] + date: new Date().toLocaleString(), + }, + ], }; case WITHDRAW: if (state.accounts.bank < action.payload) { - console.warn('Insufficient funds!'); + console.warn("Insufficient funds!"); return state; } return { @@ -159,22 +159,22 @@ function appReducer(state = initialState, action) { accounts: { ...state.accounts, bank: state.accounts.bank - action.payload, - cash: state.accounts.cash + action.payload + cash: state.accounts.cash + action.payload, }, transactions: [ ...state.transactions, { - type: 'Withdraw', + type: "Withdraw", amount: action.payload, - date: new Date().toLocaleString() - } - ] + date: new Date().toLocaleString(), + }, + ], }; case TRANSFER: const fromAccount = action.from; if (state.accounts[fromAccount] < action.payload) { - console.warn('Insufficient funds!'); + console.warn("Insufficient funds!"); return state; } @@ -187,13 +187,13 @@ function appReducer(state = initialState, action) { transactions: [ ...state.transactions, { - type: 'Transfer', + type: "Transfer", amount: action.payload, from: fromAccount, target: action.target, - date: new Date().toLocaleString() - } - ] + date: new Date().toLocaleString(), + }, + ], }; case UPDATE_ACCOUNTS: @@ -201,20 +201,20 @@ function appReducer(state = initialState, action) { ...state, accounts: { ...state.accounts, - ...action.payload - } + ...action.payload, + }, }; case UPDATE_PIN: return { ...state, - pin: String(action.payload) + pin: String(action.payload), }; - case 'SET_UID': + case "SET_UID": return { ...state, - uid: action.payload + uid: action.payload, }; default: @@ -238,13 +238,15 @@ const store = createStore(appReducer); * @param {Object} data - Event data */ function sendEvent(event, data) { - if (typeof A3API !== 'undefined') { - A3API.SendAlert(JSON.stringify({ - event: event, - data: data - })); + if (typeof A3API !== "undefined") { + A3API.SendAlert( + JSON.stringify({ + event: event, + data: data, + }), + ); } else { - console.log('Event:', event, 'Data:', data); + console.log("Event:", event, "Data:", data); } } @@ -253,7 +255,7 @@ function sendEvent(event, data) { * @param {Object} data - Account data from Arma 3 */ function syncDataFromArma(data) { - if (data && typeof data === 'object') { + if (data && typeof data === "object") { const accounts = {}; if (data.bank !== undefined) accounts.bank = data.bank; @@ -268,7 +270,7 @@ function syncDataFromArma(data) { // Update UID if provided if (data.uid !== undefined && data.uid !== store.getState().uid) { - store.dispatch({ type: 'SET_UID', payload: data.uid }); + store.dispatch({ type: "SET_UID", payload: data.uid }); } // Update pin if provided @@ -276,9 +278,12 @@ function syncDataFromArma(data) { store.dispatch(updatePin(data.pin)); } - console.log('[Store] Synced data from Arma:', store.getState().accounts); + console.log( + "[Store] Synced data from Arma:", + store.getState().accounts, + ); } else { - console.warn('[Store] Invalid data received:', data); + console.warn("[Store] Invalid data received:", data); } } @@ -287,14 +292,14 @@ function syncDataFromArma(data) { // ============================================================================ // Request initial data from Arma on load -if (typeof A3API !== 'undefined') { +if (typeof A3API !== "undefined") { // Delay request slightly to ensure everything is loaded setTimeout(() => { - sendEvent('bank::sync', {}); + sendEvent("bank::sync", {}); }, 100); } // Expose sync function globally for Arma to call -if (typeof window !== 'undefined') { +if (typeof window !== "undefined") { window.syncDataFromArma = syncDataFromArma; } diff --git a/arma/client/addons/common/README.md b/arma/client/addons/common/README.md index 9ce0564..f6c181a 100644 --- a/arma/client/addons/common/README.md +++ b/arma/client/addons/common/README.md @@ -1,4 +1,3 @@ -forge_client_common -=================== +# forge_client_common Common functionality shared between addons. diff --git a/arma/client/addons/garage/README.md b/arma/client/addons/garage/README.md index 5378813..0442fa7 100644 --- a/arma/client/addons/garage/README.md +++ b/arma/client/addons/garage/README.md @@ -1,4 +1,3 @@ -forge_client_garage -=================== +# forge_client_garage Description for this addon diff --git a/arma/client/addons/garage/ui/_site/index.html b/arma/client/addons/garage/ui/_site/index.html index 8fd7d94..ccd5165 100644 --- a/arma/client/addons/garage/ui/_site/index.html +++ b/arma/client/addons/garage/ui/_site/index.html @@ -1,195 +1,253 @@ - - - - - Vehicle Garage - - - - + const script = document.createElement("script"); + script.text = js; + document.head.appendChild(script); + }); + + - -
- -
- -
-

Vehicle Garage

-

Vehicle Management System

-
-
-
- Stored - 12 + +
+ +
+ -
- Active - 2 +
+

Vehicle Garage

+

Vehicle Management System

-
- Capacity - 20 -
-
-
- -
-
- - -
- -
-
-

Filters

-
-
- -
-

Status

-
- - - -
+
+
+ Stored + 12
- - -
-

Vehicle Type

-
- - - - - -
+
+ Active + 2
- - -
-

Search

- +
+ Capacity + 20
-
- - -
-
-

Your Vehicles

-
-
-
- -
+
+
- -
-
-

Vehicle Details

-
-
-
-
🚗
-

Select a vehicle to view details

+ +
+ +
+
+

Filters

- -