From e0e6121a5ca9f2a012cc7dfa23cf9b683b5bae66 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 7 Mar 2026 19:54:17 -0600 Subject: [PATCH] Refactor org app structure and portal interactions --- arma/client/addons/org/XEH_PREP.hpp | 1 + arma/client/addons/org/XEH_postInitClient.sqf | 26 ++ .../org/functions/fnc_buildPortalPayload.sqf | 129 ++++++ .../org/functions/fnc_handleUIEvents.sqf | 152 +------ arma/client/addons/org/ui/_site/bridge.js | 26 ++ .../org/ui/_site/components/AppShell.js | 107 +++++ .../addons/org/ui/_site/components/footer.js | 39 +- .../addons/org/ui/_site/components/forms.css | 84 ---- .../addons/org/ui/_site/components/header.js | 16 +- .../addons/org/ui/_site/components/hero.js | 35 ++ .../org/ui/_site/components/homeView.css | 12 - .../org/ui/_site/components/homeView.js | 40 -- .../addons/org/ui/_site/components/index.js | 48 -- .../org/ui/_site/components/loginForm.js | 96 ---- .../addons/org/ui/_site/components/modal.js | 190 ++++++++ .../addons/org/ui/_site/components/navbar.css | 79 ---- .../addons/org/ui/_site/components/navbar.js | 131 ++++-- .../org/ui/_site/components/panelCard.js | 83 ++++ .../_site/components/portal/activityCard.js | 79 ++++ .../ui/_site/components/portal/assetsCard.js | 94 ++++ .../ui/_site/components/portal/dangerCard.js | 70 +++ .../ui/_site/components/portal/fleetCard.js | 101 ++++ .../ui/_site/components/portal/futureCard.js | 105 +++++ .../_site/components/portal}/membersCard.js | 91 +++- .../ui/_site/components/portal/metricCard.js | 77 ++++ .../portal}/modalLayer.js | 57 +-- .../_site/components/portal}/overviewCard.js | 95 +++- .../ui/_site/components/portal/simpleStat.js | 39 ++ .../_site/components/portal/treasuryCard.js | 430 ++++++++++++++++++ .../{portal/components => }/controls.css | 0 .../components/portalHeader.css => hero.css} | 0 arma/client/addons/org/ui/_site/index.html | 62 +-- .../org/ui/_site/logic/portalActions.js | 318 +++++++++++++ .../org/ui/_site/logic/portalPermissions.js | 75 +++ .../addons/org/ui/_site/logic/portalStore.js | 38 ++ .../org/ui/_site/logic/registryStore.js | 69 +++ .../addons/org/ui/_site/portal/actions.js | 291 +----------- .../_site/portal/components/activityCard.css | 32 -- .../_site/portal/components/activityCard.js | 44 -- .../ui/_site/portal/components/assetsCard.js | 55 --- .../ui/_site/portal/components/dangerCard.css | 22 - .../ui/_site/portal/components/dangerCard.js | 56 --- .../_site/portal/components/disbandedView.js | 47 -- .../ui/_site/portal/components/fleetCard.js | 56 --- .../org/ui/_site/portal/components/footer.js | 43 -- .../ui/_site/portal/components/futureCard.css | 74 --- .../ui/_site/portal/components/futureCard.js | 45 -- .../org/ui/_site/portal/components/index.js | 65 --- .../org/ui/_site/portal/components/layout.css | 81 ---- .../ui/_site/portal/components/metricCard.css | 69 --- .../ui/_site/portal/components/metricCard.js | 20 - .../ui/_site/portal/components/modalLayer.css | 131 ------ .../_site/portal/components/overviewCard.css | 58 --- .../_site/portal/components/portalHeader.js | 30 -- .../ui/_site/portal/components/simpleList.css | 67 --- .../ui/_site/portal/components/simpleStat.css | 18 - .../ui/_site/portal/components/simpleStat.js | 15 - .../_site/portal/components/treasuryCard.css | 99 ---- .../_site/portal/components/treasuryCard.js | 126 ----- .../addons/org/ui/_site/portal/permissions.js | 71 +-- .../addons/org/ui/_site/portal/runtime.js | 98 ---- .../addons/org/ui/_site/portal/store.js | 31 +- arma/client/addons/org/ui/_site/runtime.js | 25 +- arma/client/addons/org/ui/_site/state.js | 38 +- .../org/ui/_site/views/DisbandedView.js | 36 ++ .../addons/org/ui/_site/views/HomeView.js | 89 ++++ .../addons/org/ui/_site/views/PortalView.js | 206 +++++++++ .../RegistrationView.js} | 280 ++++++++---- arma/server/addons/org/XEH_preInit.sqf | 23 + .../addons/org/functions/fnc_initOrgStore.sqf | 137 +++++- arma/ui/apps/components/AppShell.js | 107 +++++ arma/ui/apps/components/footer.js | 32 ++ arma/ui/apps/components/header.js | 27 ++ arma/ui/apps/components/hero.js | 35 ++ arma/ui/apps/components/modal.js | 190 ++++++++ arma/ui/apps/components/navbar.js | 129 ++++++ arma/ui/apps/components/panelCard.js | 83 ++++ .../ui/apps/components/portal/activityCard.js | 79 ++++ arma/ui/apps/components/portal/assetsCard.js | 94 ++++ arma/ui/apps/components/portal/dangerCard.js | 70 +++ arma/ui/apps/components/portal/fleetCard.js | 101 ++++ arma/ui/apps/components/portal/futureCard.js | 105 +++++ .../apps/components/portal}/membersCard.js | 91 +++- arma/ui/apps/components/portal/metricCard.js | 77 ++++ .../portal}/modalLayer.js | 57 +-- .../apps/components/portal}/overviewCard.js | 95 +++- arma/ui/apps/components/portal/simpleStat.js | 39 ++ .../ui/apps/components/portal/treasuryCard.js | 430 ++++++++++++++++++ .../apps/{portal/components => }/controls.css | 0 .../components/portalHeader.css => hero.css} | 0 arma/ui/apps/logic/portalActions.js | 318 +++++++++++++ arma/ui/apps/logic/portalPermissions.js | 75 +++ arma/ui/apps/logic/portalStore.js | 38 ++ arma/ui/apps/logic/registryStore.js | 69 +++ arma/ui/apps/main/bridge.js | 48 +- arma/ui/apps/main/components/footer.js | 43 -- arma/ui/apps/main/components/forms.css | 84 ---- arma/ui/apps/main/components/header.js | 23 - arma/ui/apps/main/components/homeView.css | 12 - arma/ui/apps/main/components/homeView.js | 57 --- arma/ui/apps/main/components/index.js | 48 -- arma/ui/apps/main/components/loginForm.js | 96 ---- arma/ui/apps/main/components/navbar.css | 79 ---- arma/ui/apps/main/components/navbar.js | 70 --- arma/ui/apps/main/index.html | 68 ++- arma/ui/apps/main/state.js | 38 +- arma/ui/apps/portal/actions.js | 291 +----------- .../apps/portal/components/activityCard.css | 32 -- .../ui/apps/portal/components/activityCard.js | 44 -- arma/ui/apps/portal/components/assetsCard.js | 55 --- arma/ui/apps/portal/components/dangerCard.css | 22 - arma/ui/apps/portal/components/dangerCard.js | 56 --- .../apps/portal/components/disbandedView.js | 47 -- arma/ui/apps/portal/components/fleetCard.js | 56 --- arma/ui/apps/portal/components/footer.js | 43 -- arma/ui/apps/portal/components/futureCard.css | 74 --- arma/ui/apps/portal/components/futureCard.js | 45 -- arma/ui/apps/portal/components/index.js | 65 --- arma/ui/apps/portal/components/layout.css | 81 ---- arma/ui/apps/portal/components/metricCard.css | 69 --- arma/ui/apps/portal/components/metricCard.js | 20 - arma/ui/apps/portal/components/modalLayer.css | 131 ------ .../apps/portal/components/overviewCard.css | 58 --- .../ui/apps/portal/components/portalHeader.js | 30 -- arma/ui/apps/portal/components/simpleList.css | 67 --- arma/ui/apps/portal/components/simpleStat.css | 18 - arma/ui/apps/portal/components/simpleStat.js | 15 - .../apps/portal/components/treasuryCard.css | 99 ---- .../ui/apps/portal/components/treasuryCard.js | 126 ----- arma/ui/apps/portal/permissions.js | 71 +-- arma/ui/apps/portal/runtime.js | 98 ---- arma/ui/apps/portal/store.js | 31 +- arma/ui/apps/{main => }/runtime.js | 26 +- arma/ui/apps/views/DisbandedView.js | 36 ++ arma/ui/apps/views/HomeView.js | 97 ++++ arma/ui/apps/views/PortalView.js | 206 +++++++++ .../RegistrationView.js} | 280 ++++++++---- 137 files changed, 6007 insertions(+), 5061 deletions(-) create mode 100644 arma/client/addons/org/functions/fnc_buildPortalPayload.sqf create mode 100644 arma/client/addons/org/ui/_site/components/AppShell.js delete mode 100644 arma/client/addons/org/ui/_site/components/forms.css create mode 100644 arma/client/addons/org/ui/_site/components/hero.js delete mode 100644 arma/client/addons/org/ui/_site/components/homeView.css delete mode 100644 arma/client/addons/org/ui/_site/components/homeView.js delete mode 100644 arma/client/addons/org/ui/_site/components/index.js delete mode 100644 arma/client/addons/org/ui/_site/components/loginForm.js create mode 100644 arma/client/addons/org/ui/_site/components/modal.js delete mode 100644 arma/client/addons/org/ui/_site/components/navbar.css create mode 100644 arma/client/addons/org/ui/_site/components/panelCard.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/activityCard.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/assetsCard.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/dangerCard.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/fleetCard.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/futureCard.js rename arma/{ui/apps/portal/components => client/addons/org/ui/_site/components/portal}/membersCard.js (56%) create mode 100644 arma/client/addons/org/ui/_site/components/portal/metricCard.js rename arma/client/addons/org/ui/_site/{portal/components => components/portal}/modalLayer.js (85%) rename arma/{ui/apps/portal/components => client/addons/org/ui/_site/components/portal}/overviewCard.js (67%) create mode 100644 arma/client/addons/org/ui/_site/components/portal/simpleStat.js create mode 100644 arma/client/addons/org/ui/_site/components/portal/treasuryCard.js rename arma/client/addons/org/ui/_site/{portal/components => }/controls.css (100%) rename arma/client/addons/org/ui/_site/{portal/components/portalHeader.css => hero.css} (100%) create mode 100644 arma/client/addons/org/ui/_site/logic/portalActions.js create mode 100644 arma/client/addons/org/ui/_site/logic/portalPermissions.js create mode 100644 arma/client/addons/org/ui/_site/logic/portalStore.js create mode 100644 arma/client/addons/org/ui/_site/logic/registryStore.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/activityCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/activityCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/assetsCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/dangerCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/dangerCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/disbandedView.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/fleetCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/footer.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/futureCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/futureCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/index.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/layout.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/metricCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/metricCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/modalLayer.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/overviewCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/portalHeader.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/simpleList.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/simpleStat.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/simpleStat.js delete mode 100644 arma/client/addons/org/ui/_site/portal/components/treasuryCard.css delete mode 100644 arma/client/addons/org/ui/_site/portal/components/treasuryCard.js delete mode 100644 arma/client/addons/org/ui/_site/portal/runtime.js create mode 100644 arma/client/addons/org/ui/_site/views/DisbandedView.js create mode 100644 arma/client/addons/org/ui/_site/views/HomeView.js create mode 100644 arma/client/addons/org/ui/_site/views/PortalView.js rename arma/client/addons/org/ui/_site/{components/createOrgForm.js => views/RegistrationView.js} (56%) create mode 100644 arma/ui/apps/components/AppShell.js create mode 100644 arma/ui/apps/components/footer.js create mode 100644 arma/ui/apps/components/header.js create mode 100644 arma/ui/apps/components/hero.js create mode 100644 arma/ui/apps/components/modal.js create mode 100644 arma/ui/apps/components/navbar.js create mode 100644 arma/ui/apps/components/panelCard.js create mode 100644 arma/ui/apps/components/portal/activityCard.js create mode 100644 arma/ui/apps/components/portal/assetsCard.js create mode 100644 arma/ui/apps/components/portal/dangerCard.js create mode 100644 arma/ui/apps/components/portal/fleetCard.js create mode 100644 arma/ui/apps/components/portal/futureCard.js rename arma/{client/addons/org/ui/_site/portal/components => ui/apps/components/portal}/membersCard.js (56%) create mode 100644 arma/ui/apps/components/portal/metricCard.js rename arma/ui/apps/{portal/components => components/portal}/modalLayer.js (85%) rename arma/{client/addons/org/ui/_site/portal/components => ui/apps/components/portal}/overviewCard.js (67%) create mode 100644 arma/ui/apps/components/portal/simpleStat.js create mode 100644 arma/ui/apps/components/portal/treasuryCard.js rename arma/ui/apps/{portal/components => }/controls.css (100%) rename arma/ui/apps/{portal/components/portalHeader.css => hero.css} (100%) create mode 100644 arma/ui/apps/logic/portalActions.js create mode 100644 arma/ui/apps/logic/portalPermissions.js create mode 100644 arma/ui/apps/logic/portalStore.js create mode 100644 arma/ui/apps/logic/registryStore.js delete mode 100644 arma/ui/apps/main/components/footer.js delete mode 100644 arma/ui/apps/main/components/forms.css delete mode 100644 arma/ui/apps/main/components/header.js delete mode 100644 arma/ui/apps/main/components/homeView.css delete mode 100644 arma/ui/apps/main/components/homeView.js delete mode 100644 arma/ui/apps/main/components/index.js delete mode 100644 arma/ui/apps/main/components/loginForm.js delete mode 100644 arma/ui/apps/main/components/navbar.css delete mode 100644 arma/ui/apps/main/components/navbar.js delete mode 100644 arma/ui/apps/portal/components/activityCard.css delete mode 100644 arma/ui/apps/portal/components/activityCard.js delete mode 100644 arma/ui/apps/portal/components/assetsCard.js delete mode 100644 arma/ui/apps/portal/components/dangerCard.css delete mode 100644 arma/ui/apps/portal/components/dangerCard.js delete mode 100644 arma/ui/apps/portal/components/disbandedView.js delete mode 100644 arma/ui/apps/portal/components/fleetCard.js delete mode 100644 arma/ui/apps/portal/components/footer.js delete mode 100644 arma/ui/apps/portal/components/futureCard.css delete mode 100644 arma/ui/apps/portal/components/futureCard.js delete mode 100644 arma/ui/apps/portal/components/index.js delete mode 100644 arma/ui/apps/portal/components/layout.css delete mode 100644 arma/ui/apps/portal/components/metricCard.css delete mode 100644 arma/ui/apps/portal/components/metricCard.js delete mode 100644 arma/ui/apps/portal/components/modalLayer.css delete mode 100644 arma/ui/apps/portal/components/overviewCard.css delete mode 100644 arma/ui/apps/portal/components/portalHeader.js delete mode 100644 arma/ui/apps/portal/components/simpleList.css delete mode 100644 arma/ui/apps/portal/components/simpleStat.css delete mode 100644 arma/ui/apps/portal/components/simpleStat.js delete mode 100644 arma/ui/apps/portal/components/treasuryCard.css delete mode 100644 arma/ui/apps/portal/components/treasuryCard.js delete mode 100644 arma/ui/apps/portal/runtime.js rename arma/ui/apps/{main => }/runtime.js (79%) create mode 100644 arma/ui/apps/views/DisbandedView.js create mode 100644 arma/ui/apps/views/HomeView.js create mode 100644 arma/ui/apps/views/PortalView.js rename arma/ui/apps/{main/components/createOrgForm.js => views/RegistrationView.js} (56%) diff --git a/arma/client/addons/org/XEH_PREP.hpp b/arma/client/addons/org/XEH_PREP.hpp index d83118a..438a579 100644 --- a/arma/client/addons/org/XEH_PREP.hpp +++ b/arma/client/addons/org/XEH_PREP.hpp @@ -1,3 +1,4 @@ +PREP(buildPortalPayload); PREP(handleUIEvents); PREP(initOrgClass); PREP(openUI); diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf index 7ba023a..515ba5e 100644 --- a/arma/client/addons/org/XEH_postInitClient.sqf +++ b/arma/client/addons/org/XEH_postInitClient.sqf @@ -18,6 +18,32 @@ if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); }; GVAR(OrgClass) call ["sync", [_data, _jip]]; }] call CFUNC(addEventHandler); +[QGVAR(responseCreateOrg), { + params [["_payload", createHashMap, [createHashMap]]]; + + private _control = uiNamespace getVariable [QGVAR(PendingBrowserControl), controlNull]; + uiNamespace setVariable [QGVAR(PendingBrowserControl), controlNull]; + + private _success = _payload getOrDefault ["success", false]; + if (!_success) exitWith { + if (_control isNotEqualTo controlNull) then { + private _json = toJSON (createHashMapFromArray [ + ["message", _payload getOrDefault ["message", "Organization registration failed."]] + ]); + + _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateFailure(%1)", _json]]; + }; + }; + + private _orgData = _payload getOrDefault ["org", createHashMap]; + GVAR(OrgClass) call ["sync", [_orgData, true]]; + + if (_control isNotEqualTo controlNull) then { + private _json = toJSON (call FUNC(buildPortalPayload)); + _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateSuccess(%1)", _json]]; + }; +}] call CFUNC(addEventHandler); + [{ EGVAR(actor,ActorClass) get "isLoaded"; }, { diff --git a/arma/client/addons/org/functions/fnc_buildPortalPayload.sqf b/arma/client/addons/org/functions/fnc_buildPortalPayload.sqf new file mode 100644 index 0000000..1fb17bf --- /dev/null +++ b/arma/client/addons/org/functions/fnc_buildPortalPayload.sqf @@ -0,0 +1,129 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Builds the web portal payload from the synced org class. + * + * Arguments: + * None + * + * Return Value: + * Portal payload + * + * Example: + * call forge_client_org_fnc_buildPortalPayload; + * + * Public: No + */ + +private _orgData = GVAR(OrgClass) get "org"; + +private _name = _orgData getOrDefault ["name", "Unknown Organization"]; +private _id = _orgData getOrDefault ["id", ""]; +private _ownerUid = _orgData getOrDefault ["owner", ""]; +private _funds = _orgData getOrDefault ["funds", 0]; +private _reputation = _orgData getOrDefault ["reputation", 0]; +private _assetsRaw = _orgData getOrDefault ["assets", createHashMap]; +private _membersRaw = _orgData getOrDefault ["members", createHashMap]; +private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap]; +private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"]; +private _type = _orgData getOrDefault ["type", "Organization"]; +private _status = _orgData getOrDefault ["status", "Operational"]; +private _isDefaultOrg = (_orgData getOrDefault ["default", false]) + || {toLower _id isEqualTo "default"} + || {toLower _ownerUid isEqualTo "server"}; + +private _playerName = name player; +private _playerUid = getPlayerUID player; +private _playerVar = vehicleVarName player; +private _sessionRole = "Member"; +private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"}; +private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server"); + +private _membersList = []; +{ + private _memberData = _y; + private _memberName = _memberData getOrDefault ["name", "Unknown"]; + private _memberUid = _memberData getOrDefault ["uid", ""]; + + if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; }; + if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; }; + + _membersList pushBack (createHashMapFromArray [["name", _memberName]]); +} forEach _membersRaw; + +if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; }; +if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; }; +if (_ownerUid isEqualTo _playerUid) then { _sessionRole = "Leader"; }; + +private _assetsList = []; +{ + private _assetData = _y; + _assetsList pushBack (createHashMapFromArray [ + ["name", _assetData getOrDefault ["name", "Unknown Asset"]], + ["type", _assetData getOrDefault ["type", "items"]], + ["quantity", str (_assetData getOrDefault ["quantity", 0])] + ]); +} forEach _assetsRaw; + +private _fleetList = []; +{ + private _vehicleData = _y; + _fleetList pushBack (createHashMapFromArray [ + ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], + ["type", _vehicleData getOrDefault ["type", "other"]], + ["status", _vehicleData getOrDefault ["status", "Unknown"]], + ["damage", _vehicleData getOrDefault ["damage", "0%"]] + ]); +} forEach _fleetRaw; + +private _roadmap = [ + createHashMapFromArray [ + ["name", "Contracts Board"], + ["status", "Planned"], + ["detail", "Track payouts, assignments, and claim approvals."] + ], + createHashMapFromArray [ + ["name", "Diplomacy"], + ["status", "Future Review"], + ["detail", "Possible future module pending a full design and scope review."] + ], + createHashMapFromArray [ + ["name", "Logistics Queue"], + ["status", "Future Review"], + ["detail", "Possible future module pending a full design and scope review."] + ], + createHashMapFromArray [ + ["name", "Permissions"], + ["status", "Future Review"], + ["detail", "Possible future module pending a full design and scope review."] + ] +]; + +createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["actorName", _playerName], + ["actorUid", _playerUid], + ["role", _sessionRole], + ["ceo", _sessionIsCeo] + ]], + ["portalData", createHashMapFromArray [ + ["org", createHashMapFromArray [ + ["name", _name], + ["tag", _id], + ["type", _type], + ["status", _status], + ["headquarters", _headquarters], + ["owner", _ownerName], + ["ownerUid", _ownerUid], + ["isDefault", _isDefaultOrg] + ]], + ["funds", _funds], + ["reputation", _reputation], + ["members", _membersList], + ["fleet", _fleetList], + ["assets", _assetsList], + ["activity", []], + ["roadmap", _roadmap] + ]] +] diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf index 10a12ea..94e713c 100644 --- a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf @@ -30,159 +30,35 @@ private _fnc_execBridge = { _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]]; }; -private _fnc_buildPortalPayload = { - private _orgData = GVAR(OrgClass) get "org"; - - private _name = _orgData getOrDefault ["name", "Unknown Organization"]; - private _id = _orgData getOrDefault ["id", ""]; - private _ownerUid = _orgData getOrDefault ["owner", ""]; - private _funds = _orgData getOrDefault ["funds", 0]; - private _reputation = _orgData getOrDefault ["reputation", 0]; - private _assetsRaw = _orgData getOrDefault ["assets", createHashMap]; - private _membersRaw = _orgData getOrDefault ["members", createHashMap]; - private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap]; - private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"]; - private _type = _orgData getOrDefault ["type", "Organization"]; - private _status = _orgData getOrDefault ["status", "Operational"]; - private _isDefaultOrg = (_orgData getOrDefault ["default", false]) - || {toLower _id isEqualTo "default"} - || {toLower _ownerUid isEqualTo "server"}; - - private _playerName = name player; - private _playerUid = getPlayerUID player; - private _playerVar = vehicleVarName player; - private _sessionRole = "Member"; - private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"}; - private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server"); - - private _membersList = []; - { - private _memberData = _y; - private _memberName = _memberData getOrDefault ["name", "Unknown"]; - private _memberUid = _memberData getOrDefault ["uid", ""]; - - if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { - _ownerName = _memberName; - }; - - if (_memberUid isEqualTo _playerUid) then { - _sessionRole = "Member"; - }; - - _membersList pushBack (createHashMapFromArray [ - ["name", _memberName] - ]); - } forEach _membersRaw; - - if (_ownerName isEqualTo "" && {_ownerUid isEqualTo _playerUid}) then { - _ownerName = _playerName; - }; - - if (_ownerName isEqualTo "" && {_ownerUid isNotEqualTo ""}) then { - _ownerName = "Unknown Owner"; - }; - - if (_ownerUid isEqualTo _playerUid) then { - _sessionRole = "Leader"; - }; - - private _assetsList = []; - { - private _assetData = _y; - _assetsList pushBack (createHashMapFromArray [ - ["name", _assetData getOrDefault ["name", "Unknown Asset"]], - ["type", _assetData getOrDefault ["type", "items"]], - ["quantity", str (_assetData getOrDefault ["quantity", 0])] - ]); - } forEach _assetsRaw; - - private _fleetList = []; - { - private _vehicleData = _y; - _fleetList pushBack (createHashMapFromArray [ - ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], - ["type", _vehicleData getOrDefault ["type", "other"]], - ["status", _vehicleData getOrDefault ["status", "Unknown"]], - ["damage", _vehicleData getOrDefault ["damage", "0%"]] - ]); - } forEach _fleetRaw; - - private _roadmap = [ - createHashMapFromArray [ - ["name", "Contracts Board"], - ["status", "Planned"], - ["detail", "Track payouts, assignments, and claim approvals."] - ], - createHashMapFromArray [ - ["name", "Diplomacy"], - ["status", "Future Review"], - ["detail", "Possible future module pending a full design and scope review."] - ], - createHashMapFromArray [ - ["name", "Logistics Queue"], - ["status", "Future Review"], - ["detail", "Possible future module pending a full design and scope review."] - ], - createHashMapFromArray [ - ["name", "Permissions"], - ["status", "Future Review"], - ["detail", "Possible future module pending a full design and scope review."] - ] - ]; - - createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["actorName", _playerName], - ["actorUid", _playerUid], - ["role", _sessionRole], - ["ceo", _sessionIsCeo] - ]], - ["portalData", createHashMapFromArray [ - ["org", createHashMapFromArray [ - ["name", _name], - ["tag", _id], - ["type", _type], - ["status", _status], - ["headquarters", _headquarters], - ["owner", _ownerName], - ["ownerUid", _ownerUid], - ["isDefault", _isDefaultOrg] - ]], - ["funds", _funds], - ["reputation", _reputation], - ["members", _membersList], - ["fleet", _fleetList], - ["assets", _assetsList], - ["activity", []], - ["roadmap", _roadmap] - ]] - ] -}; - diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data]; switch (_event) do { case "org::close": { closeDialog 1; }; case "org::login::request": { - private _email = toLower (_data getOrDefault ["email", ""]); - private _password = _data getOrDefault ["password", ""]; private _orgData = GVAR(OrgClass) get "org"; private _orgId = _orgData getOrDefault ["id", ""]; private _orgName = _orgData getOrDefault ["name", ""]; - if (_email isEqualTo "" || {_password isEqualTo ""}) exitWith { - [_control, "receiveLoginFailure", createHashMapFromArray [ - ["message", "Enter both email and password."] - ]] call _fnc_execBridge; - }; - if (_orgId isEqualTo "" && {_orgName isEqualTo ""}) exitWith { [_control, "receiveLoginFailure", createHashMapFromArray [ ["message", "No organization data is available for this player."] ]] call _fnc_execBridge; }; - [_control, "receiveLoginSuccess", call _fnc_buildPortalPayload] call _fnc_execBridge; + private _payload = call FUNC(buildPortalPayload); + [_control, "receiveLoginSuccess", _payload] call _fnc_execBridge; + }; + case "org::create::request": { + private _orgName = _data getOrDefault ["orgName", ""]; + + if (_orgName isEqualTo "") exitWith { + [_control, "receiveCreateFailure", createHashMapFromArray [ + ["message", "Enter an organization name."] + ]] call _fnc_execBridge; + }; + + uiNamespace setVariable [QGVAR(PendingBrowserControl), _control]; + [SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent); }; case "org::ready": { [_control, "receive", createHashMapFromArray [ diff --git a/arma/client/addons/org/ui/_site/bridge.js b/arma/client/addons/org/ui/_site/bridge.js index 34ab232..b4afb78 100644 --- a/arma/client/addons/org/ui/_site/bridge.js +++ b/arma/client/addons/org/ui/_site/bridge.js @@ -30,6 +30,17 @@ store.failLogin("Arma login bridge is unavailable."); } + function requestCreateOrg(registration) { + store.startCreate(); + + const sent = sendEvent("org::create::request", registration); + if (sent) { + return; + } + + store.failCreate("Arma registration bridge is unavailable."); + } + function receive(eventOrPayload, data = {}) { const event = typeof eventOrPayload === "object" && eventOrPayload !== null @@ -49,18 +60,33 @@ store.failLogin(payloadData.message || "Authentication failed."); return; } + + if (event === "org::create::success") { + store.completeCreate(payloadData); + return; + } + + if (event === "org::create::failure") { + store.failCreate( + payloadData.message || "Organization registration failed.", + ); + } } RegistryApp.bridge = { requestLogin, + requestCreateOrg, receive, sendEvent, }; window.OrgUIBridge = { requestLogin, + requestCreateOrg, receive, receiveLoginSuccess: (data) => receive("org::login::success", data), receiveLoginFailure: (data) => receive("org::login::failure", data), + receiveCreateSuccess: (data) => receive("org::create::success", data), + receiveCreateFailure: (data) => receive("org::create::failure", data), }; })(); diff --git a/arma/client/addons/org/ui/_site/components/AppShell.js b/arma/client/addons/org/ui/_site/components/AppShell.js new file mode 100644 index 0000000..aa56bfc --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/AppShell.js @@ -0,0 +1,107 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h } = RegistryApp.runtime; + const store = RegistryApp.store; + + RegistryApp.components = RegistryApp.components || {}; + + RegistryApp.components.App = function App() { + const Navbar = window.SharedUI.componentFns.Navbar; + const Header = window.SharedUI.componentFns.Header; + const Footer = window.SharedUI.componentFns.Footer; + const HomeView = RegistryApp.componentFns.HomeView; + const RegistrationView = RegistryApp.componentFns.RegistrationView; + const PortalApp = + window.OrgPortal && window.OrgPortal.components + ? window.OrgPortal.components.App + : null; + + const view = store.getView(); + const viewLabel = + view === "create" + ? "Organization Registration" + : view === "portal" + ? "Organization Portal" + : "Entry Hub"; + const actionLabel = view === "portal" ? "Sign Out" : "Close"; + const footerSections = [ + { + title: "Registry Resources", + items: [ + "Registration Guidelines", + "Tax & Fee Schedule", + "Legal Compliance", + "Trademark Database", + ], + }, + { + title: "Bureau Support", + items: [ + "Office: Sector 7 Admin Block", + "Hours: 0800 - 1600 (GST)", + "Helpdesk: 555-01-REGISTRY", + "support@org-bureau.gov", + ], + }, + ]; + + function closeRegistry() { + if ( + typeof A3API !== "undefined" && + typeof A3API.SendAlert === "function" + ) { + A3API.SendAlert( + JSON.stringify({ + event: "org::close", + data: {}, + }), + ); + return; + } + + store.setView("home"); + } + + if (view === "portal" && PortalApp) { + return h( + "div", + null, + Navbar({ + title: "Global Organization Network", + viewLabel, + actionLabel, + onAction: closeRegistry, + }), + PortalApp(), + ); + } + + let mainContent; + if (view === "home") { + mainContent = HomeView(); + } else if (view === "create") { + mainContent = RegistrationView(); + } + + return h( + "main", + null, + Navbar({ + title: "Global Organization Network", + viewLabel, + actionLabel, + onAction: closeRegistry, + }), + h( + "div", + { className: "container" }, + Header({ + title: "Global Organization Network", + onTitleClick: () => store.setView("home"), + }), + mainContent, + ), + Footer({ sections: footerSections }), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/footer.js b/arma/client/addons/org/ui/_site/components/footer.js index 76f53c4..4401f4b 100644 --- a/arma/client/addons/org/ui/_site/components/footer.js +++ b/arma/client/addons/org/ui/_site/components/footer.js @@ -1,40 +1,29 @@ (function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); const { h } = RegistryApp.runtime; - RegistryApp.componentFns = RegistryApp.componentFns || {}; + SharedUI.componentFns = SharedUI.componentFns || {}; - RegistryApp.componentFns.Footer = function Footer() { + SharedUI.componentFns.Footer = function Footer({ sections = [] }) { return h( "div", { className: "footer" }, h( "div", { className: "wrapper" }, - h( - "div", - null, - h("h3", null, "Registry Resources"), + ...sections.map((section) => h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "Registration Guidelines"), - h("li", null, "Tax & Fee Schedule"), - h("li", null, "Legal Compliance"), - h("li", null, "Trademark Database"), - ), - ), - h( - "div", - null, - h("h3", null, "Bureau Support"), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "Office: Sector 7 Admin Block"), - h("li", null, "Hours: 0800 - 1600 (GST)"), - h("li", null, "Helpdesk: 555-01-REGISTRY"), - h("li", null, "support@org-bureau.gov"), + "div", + null, + h("h3", null, section.title), + h( + "ul", + { style: { listStyleType: "none", padding: 0 } }, + ...(section.items || []).map((item) => + h("li", null, item), + ), + ), ), ), ), diff --git a/arma/client/addons/org/ui/_site/components/forms.css b/arma/client/addons/org/ui/_site/components/forms.css deleted file mode 100644 index 1ddcc5e..0000000 --- a/arma/client/addons/org/ui/_site/components/forms.css +++ /dev/null @@ -1,84 +0,0 @@ -.split-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; - width: 100%; -} - -.info-panel { - text-align: left; - padding: 1rem; -} - -.app-form { - display: flex; - flex-direction: column; - gap: 1rem; - text-align: left; - - label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; - } - - input, - select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s; - - &:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); - } - } -} - -.form-actions { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; -} - -.cancel-link { - font-size: 0.9rem; - color: var(--text-muted); - cursor: pointer; - text-decoration: underline; - - &:hover { - color: var(--primary); - } -} - -.form-feedback { - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; - - &.is-error { - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; - } -} - -@media (max-width: 960px) { - .split-container { - grid-template-columns: 1fr; - } -} diff --git a/arma/client/addons/org/ui/_site/components/header.js b/arma/client/addons/org/ui/_site/components/header.js index bfb199c..6734ac1 100644 --- a/arma/client/addons/org/ui/_site/components/header.js +++ b/arma/client/addons/org/ui/_site/components/header.js @@ -1,23 +1,27 @@ (function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - RegistryApp.componentFns = RegistryApp.componentFns || {}; + SharedUI.componentFns = SharedUI.componentFns || {}; - RegistryApp.componentFns.Header = function Header({ title }) { + SharedUI.componentFns.Header = function Header({ + title, + subtitle = "Organization Registration & Management Portal", + onTitleClick = null, + }) { return h( "div", { className: "header" }, h( "h1", { - style: { cursor: "pointer" }, - onClick: () => store.setView("home"), + style: { cursor: onTitleClick ? "pointer" : "default" }, + onClick: onTitleClick, }, title, ), - h("p", null, "Organization Registration & Management Portal"), + h("p", null, subtitle), ); }; })(); diff --git a/arma/client/addons/org/ui/_site/components/hero.js b/arma/client/addons/org/ui/_site/components/hero.js new file mode 100644 index 0000000..a022e70 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/hero.js @@ -0,0 +1,35 @@ +(function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + + SharedUI.componentFns = SharedUI.componentFns || {}; + + SharedUI.componentFns.Hero = function Hero({ + className = "", + kicker = "", + title = "", + subtitle = "", + meta = "", + }) { + const finalClassName = [ + "card org-panel org-span-12 org-page-header", + className, + ] + .filter(Boolean) + .join(" "); + + return h( + "section", + { className: finalClassName }, + h( + "div", + { className: "org-page-heading" }, + h("span", { className: "org-page-kicker" }, kicker), + h("h1", { className: "org-page-title" }, title), + h("p", { className: "org-page-subtitle" }, subtitle), + h("span", { className: "org-page-meta" }, meta), + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/homeView.css b/arma/client/addons/org/ui/_site/components/homeView.css deleted file mode 100644 index fb1fd66..0000000 --- a/arma/client/addons/org/ui/_site/components/homeView.css +++ /dev/null @@ -1,12 +0,0 @@ -.content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -@media (max-width: 960px) { - .content { - grid-template-columns: 1fr; - } -} diff --git a/arma/client/addons/org/ui/_site/components/homeView.js b/arma/client/addons/org/ui/_site/components/homeView.js deleted file mode 100644 index 0a7bde7..0000000 --- a/arma/client/addons/org/ui/_site/components/homeView.js +++ /dev/null @@ -1,40 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.HomeView = function HomeView() { - return h( - "div", - { className: "content" }, - h( - "div", - { className: "card" }, - h("h2", null, "Create Organization"), - h( - "p", - null, - "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", - ), - h( - "button", - { onClick: () => store.setView("create") }, - "Register", - ), - ), - h( - "div", - { className: "card" }, - h("h2", null, "Organization Portal"), - h( - "p", - null, - "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", - ), - h("button", { onClick: () => store.setView("login") }, "Login"), - ), - ); - }; -})(); diff --git a/arma/client/addons/org/ui/_site/components/index.js b/arma/client/addons/org/ui/_site/components/index.js deleted file mode 100644 index 8b63cea..0000000 --- a/arma/client/addons/org/ui/_site/components/index.js +++ /dev/null @@ -1,48 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - - RegistryApp.components = RegistryApp.components || {}; - - RegistryApp.components.App = function App() { - const Navbar = RegistryApp.componentFns.Navbar; - const Header = RegistryApp.componentFns.Header; - const HomeView = RegistryApp.componentFns.HomeView; - const LoginForm = RegistryApp.componentFns.LoginForm; - const CreateOrgForm = RegistryApp.componentFns.CreateOrgForm; - const Footer = RegistryApp.componentFns.Footer; - const PortalApp = - window.OrgPortal && window.OrgPortal.components - ? window.OrgPortal.components.App - : null; - - const view = store.getView(); - - if (view === "portal" && PortalApp) { - return h("div", null, Navbar(), PortalApp()); - } - - let mainContent; - if (view === "home") { - mainContent = HomeView(); - } else if (view === "login") { - mainContent = LoginForm(); - } else if (view === "create") { - mainContent = CreateOrgForm(); - } - - return h( - "main", - null, - Navbar(), - h( - "div", - { className: "container" }, - Header({ title: "Global Organization Network" }), - mainContent, - ), - Footer(), - ); - }; -})(); diff --git a/arma/client/addons/org/ui/_site/components/loginForm.js b/arma/client/addons/org/ui/_site/components/loginForm.js deleted file mode 100644 index f27c588..0000000 --- a/arma/client/addons/org/ui/_site/components/loginForm.js +++ /dev/null @@ -1,96 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.LoginForm = function LoginForm() { - const bridge = RegistryApp.bridge; - const isAuthenticating = store.getIsAuthenticating(); - const loginError = store.getLoginError(); - - const handleLogin = () => { - const data = { - email: String( - document.getElementById("org-login-email")?.value || "", - ), - password: String( - document.getElementById("org-login-password")?.value || "", - ), - }; - - if (!bridge) { - store.failLogin("Login bridge is not available."); - return; - } - - bridge.requestLogin(data); - }; - - return h( - "div", - { - className: "card", - style: { maxWidth: "400px", margin: "0 auto" }, - }, - h("h2", null, "Organization Login"), - h( - "div", - { className: "app-form" }, - h( - "div", - null, - h("label", null, "Email"), - h("input", { - id: "org-login-email", - type: "text", - placeholder: "admin@spearnet.mil", - }), - ), - h( - "div", - null, - h("label", null, "Password"), - h("input", { - id: "org-login-password", - type: "password", - placeholder: "********", - disabled: isAuthenticating, - }), - ), - loginError - ? h( - "div", - { className: "form-feedback is-error" }, - loginError, - ) - : null, - h( - "div", - { className: "form-actions" }, - h( - "button", - { - type: "button", - style: { width: "100%" }, - onClick: handleLogin, - disabled: isAuthenticating, - }, - isAuthenticating - ? "Authenticating..." - : "Access Authenticator", - ), - h( - "span", - { - className: "cancel-link", - onClick: () => store.setView("home"), - }, - "Cancel / Return to Main", - ), - ), - ), - ); - }; -})(); diff --git a/arma/client/addons/org/ui/_site/components/modal.js b/arma/client/addons/org/ui/_site/components/modal.js new file mode 100644 index 0000000..862b115 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/modal.js @@ -0,0 +1,190 @@ +(function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h, ensureScopedStyle } = RegistryApp.runtime; + const scopeAttr = "data-ui-modal"; + const scopeSelector = `[${scopeAttr}]`; + const modalCss = ` +${scopeSelector} { + position: fixed; + inset: 0; + background: rgb(15 23 42 / 0.38); + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + z-index: 20; +} + +${scopeSelector} .app-modal-card { + width: min(100%, 30rem); + margin-bottom: 0; + text-align: left; +} + +${scopeSelector} .app-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +${scopeSelector} .app-modal-title { + margin: 0; + color: var(--primary-hover); + font-size: 1.45rem; +} + +${scopeSelector} .app-modal-close { + width: 2.25rem; + height: 2.25rem; + padding: 0; + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border); + box-shadow: none; + transform: none; +} + +${scopeSelector} .app-modal-close:hover { + background: var(--bg-surface-hover); + color: var(--text-main); + box-shadow: none; + transform: none; +} + +${scopeSelector} .app-modal-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +${scopeSelector} .app-modal-form label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; +} + +${scopeSelector} .app-modal-form input, +${scopeSelector} .app-modal-form select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; +} + +${scopeSelector} .app-modal-form input:focus, +${scopeSelector} .app-modal-form select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); +} + +${scopeSelector} .app-modal-form input:disabled, +${scopeSelector} .app-modal-form select:disabled { + background: #f1f5f9; + color: var(--text-muted); + cursor: not-allowed; +} + +${scopeSelector} .app-modal-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +${scopeSelector} .app-modal-actions button + button, +${scopeSelector} .app-modal-danger-actions button + button { + margin-left: 0; +} + +${scopeSelector} .app-modal-danger { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid #fecaca; + border-radius: var(--radius); + background: #fff1f2; + align-items: flex-start; +} + +${scopeSelector} .app-modal-danger p { + margin: 0; + color: var(--text-main); +} + +${scopeSelector} .app-modal-danger-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +@media (max-width: 960px) { + ${scopeSelector} .app-modal-head, + ${scopeSelector} .app-modal-danger { + flex-direction: column; + align-items: flex-start; + } +} +`; + + SharedUI.componentFns = SharedUI.componentFns || {}; + + SharedUI.componentFns.Modal = function Modal({ + title = "", + body = null, + onClose = null, + }) { + ensureScopedStyle("shared-modal", modalCss); + + return h( + "div", + { + className: "app-modal-backdrop", + [scopeAttr]: "", + onClick: (e) => { + if (e.target === e.currentTarget && onClose) { + onClose(); + } + }, + }, + h( + "div", + { className: "card app-modal-card" }, + h( + "div", + { className: "app-modal-head" }, + h( + "div", + null, + h("h2", { className: "app-modal-title" }, title), + ), + h( + "button", + { + type: "button", + className: "app-modal-close", + onClick: onClose, + "aria-label": "Close dialog", + }, + "x", + ), + ), + body, + ), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/navbar.css b/arma/client/addons/org/ui/_site/components/navbar.css deleted file mode 100644 index 9a6478e..0000000 --- a/arma/client/addons/org/ui/_site/components/navbar.css +++ /dev/null @@ -1,79 +0,0 @@ -.app-navbar { - background: var(--bg-surface); - border-bottom: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.app-navbar-inner { - display: flex; - justify-content: space-between; - align-items: center; - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 1rem 2rem; - box-sizing: border-box; -} - -.app-navbar-brand { - display: flex; - flex-direction: column; - gap: 0.125rem; -} - -.app-navbar-kicker { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); - font-weight: 600; -} - -.app-navbar-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary-hover); - letter-spacing: -0.025em; -} - -.app-navbar-actions { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.app-navbar-view { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - font-weight: 600; -} - -.app-close-btn { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); - padding: 0.5rem 1rem; - font-size: 0.85rem; - - &:hover { - background: var(--bg-surface-hover); - color: var(--primary-hover); - border-color: var(--primary); - transform: none; - box-shadow: none; - } -} - -@media (max-width: 960px) { - .app-navbar-inner { - flex-direction: column; - align-items: flex-start; - padding: 1rem 1.5rem; - } - - .app-navbar-actions { - align-items: flex-start; - } -} diff --git a/arma/client/addons/org/ui/_site/components/navbar.js b/arma/client/addons/org/ui/_site/components/navbar.js index 53a7b5a..2b096bb 100644 --- a/arma/client/addons/org/ui/_site/components/navbar.js +++ b/arma/client/addons/org/ui/_site/components/navbar.js @@ -1,54 +1,113 @@ (function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; + const { h, ensureScopedStyle } = RegistryApp.runtime; + const scopeAttr = "data-ui-navbar"; + const scopeSelector = `[${scopeAttr}]`; + const navbarCss = ` +${scopeSelector} { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); +} - RegistryApp.componentFns = RegistryApp.componentFns || {}; +${scopeSelector} .app-navbar-inner { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 1rem 2rem; + box-sizing: border-box; +} - function closeRegistry() { - if ( - typeof A3API !== "undefined" && - typeof A3API.SendAlert === "function" - ) { - A3API.SendAlert( - JSON.stringify({ - event: "org::close", - data: {}, - }), - ); - return; - } +${scopeSelector} .app-navbar-brand { + display: flex; + flex-direction: column; + gap: 0.125rem; +} - store.setView("home"); +${scopeSelector} .app-navbar-kicker { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; +} + +${scopeSelector} .app-navbar-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--primary-hover); + letter-spacing: -0.025em; +} + +${scopeSelector} .app-navbar-actions { + display: flex; + align-items: center; + gap: 1.5rem; +} + +${scopeSelector} .app-navbar-view { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + font-weight: 600; +} + +${scopeSelector} .app-close-btn { + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border); + padding: 0.5rem 1rem; + font-size: 0.85rem; +} + +${scopeSelector} .app-close-btn:hover { + background: var(--bg-surface-hover); + color: var(--primary-hover); + border-color: var(--primary); + transform: none; + box-shadow: none; +} + +@media (max-width: 960px) { + ${scopeSelector} .app-navbar-inner { + flex-direction: column; + align-items: flex-start; + padding: 1rem 1.5rem; } - RegistryApp.componentFns.Navbar = function Navbar() { - const view = store.getView(); - const viewLabel = - view === "login" - ? "Organization Login" - : view === "create" - ? "Organization Registration" - : view === "portal" - ? "Organization Portal" - : "Entry Hub"; - const actionLabel = view === "portal" ? "Sign Out" : "Close"; + ${scopeSelector} .app-navbar-actions { + align-items: flex-start; + } +} +`; + + SharedUI.componentFns = SharedUI.componentFns || {}; + + SharedUI.componentFns.Navbar = function Navbar({ + kicker = "ORBIS", + title = "", + viewLabel = "", + actionLabel = "", + onAction = null, + }) { + ensureScopedStyle("shared-navbar", navbarCss); return h( "nav", - { className: "app-navbar" }, + { className: "app-navbar", [scopeAttr]: "" }, h( "div", { className: "app-navbar-inner" }, h( "div", { className: "app-navbar-brand" }, - h("span", { className: "app-navbar-kicker" }, "ORBIS"), - h( - "span", - { className: "app-navbar-title" }, - "Global Organization Network", - ), + h("span", { className: "app-navbar-kicker" }, kicker), + h("span", { className: "app-navbar-title" }, title), ), h( "div", @@ -59,7 +118,7 @@ { type: "button", className: "app-close-btn", - onClick: closeRegistry, + onClick: onAction, }, actionLabel, ), diff --git a/arma/client/addons/org/ui/_site/components/panelCard.js b/arma/client/addons/org/ui/_site/components/panelCard.js new file mode 100644 index 0000000..b927c98 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/panelCard.js @@ -0,0 +1,83 @@ +(function () { + const SharedUI = (window.SharedUI = window.SharedUI || {}); + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h, ensureScopedStyle } = RegistryApp.runtime; + const scopeAttr = "data-ui-panel-card"; + const scopeSelector = `[${scopeAttr}]`; + const panelCardCss = ` +${scopeSelector} .org-panel-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +${scopeSelector} .org-eyebrow { + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.4rem; +} + +${scopeSelector} .org-panel-title { + margin: 0; + color: var(--primary-hover); + font-size: 1.45rem; +} + +${scopeSelector} .org-panel-subtitle { + margin: 0.35rem 0 0; + color: var(--text-muted); + font-size: 0.95rem; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-panel-head { + flex-direction: column; + align-items: flex-start; + } +} +`; + + SharedUI.componentFns = SharedUI.componentFns || {}; + + SharedUI.componentFns.PanelCard = function PanelCard({ + className = "", + eyebrow = "", + title = "", + subtitle = "", + headerExtras = null, + body = null, + rootProps = {}, + }) { + const finalClassName = ["card org-panel", className] + .filter(Boolean) + .join(" "); + ensureScopedStyle("shared-panel-card", panelCardCss); + + return h( + "section", + { className: finalClassName, [scopeAttr]: "", ...rootProps }, + h( + "div", + { className: "org-panel-head" }, + h( + "div", + null, + eyebrow + ? h("div", { className: "org-eyebrow" }, eyebrow) + : null, + h("h2", { className: "org-panel-title" }, title), + subtitle + ? h("p", { className: "org-panel-subtitle" }, subtitle) + : null, + ), + headerExtras, + ), + body, + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/activityCard.js b/arma/client/addons/org/ui/_site/components/portal/activityCard.js new file mode 100644 index 0000000..6060f41 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/activityCard.js @@ -0,0 +1,79 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const scopeAttr = "data-ui-activity-card"; + const scopeSelector = `[${scopeAttr}]`; + const activityCardCss = ` +${scopeSelector} .org-activity-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +${scopeSelector} .org-activity-row { + padding: 1rem; + border: 1px solid var(--border); + border-left: 3px solid #94a3b8; + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-activity-row:nth-child(even) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(148 163 184 / 0.45); + border-left-color: #64748b; +} + +${scopeSelector} .org-activity-row p { + margin: 0; + color: var(--text-main); +} + +${scopeSelector} .org-activity-time { + display: inline-block; + margin-bottom: 0.35rem; + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.ActivityCard = function ActivityCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + ensureScopedStyle("portal-activity-card", activityCardCss); + + return PanelCard({ + className: "org-scroll-panel org-span-6", + title: "Command Feed", + subtitle: "Recent organization-level actions and updates.", + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + { className: "org-activity-list" }, + ...portalData.activity.map((item) => + h( + "article", + { className: "org-activity-row" }, + h( + "span", + { className: "org-activity-time" }, + item.time, + ), + h("p", null, item.text), + ), + ), + ), + }); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/assetsCard.js b/arma/client/addons/org/ui/_site/components/portal/assetsCard.js new file mode 100644 index 0000000..8b65094 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/assetsCard.js @@ -0,0 +1,94 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + const scopeAttr = "data-ui-assets-card"; + const scopeSelector = `[${scopeAttr}]`; + const assetsCardCss = ` +${scopeSelector} .org-simple-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +${scopeSelector} .org-simple-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-simple-row:nth-child(even) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(148 163 184 / 0.45); +} + +${scopeSelector} .org-simple-name { + color: var(--primary-hover); +} + +${scopeSelector} .org-simple-meta { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 1rem; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-simple-row { + flex-direction: column; + align-items: flex-start; + } +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.AssetsCard = function AssetsCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + const SimpleStat = OrgPortal.componentFns.SimpleStat; + ensureScopedStyle("portal-assets-card", assetsCardCss); + + return PanelCard({ + className: "org-scroll-panel org-span-7", + title: "Assets", + subtitle: "Inventory supplies and equipment with quantity totals.", + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + { className: "org-simple-list" }, + ...portalData.assets.map((asset) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + asset.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatAssetType(asset.type), + ), + SimpleStat("Quantity", asset.quantity), + ), + ), + ), + ), + }); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/dangerCard.js b/arma/client/addons/org/ui/_site/components/portal/dangerCard.js new file mode 100644 index 0000000..4c825c3 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/dangerCard.js @@ -0,0 +1,70 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + const scopeAttr = "data-ui-danger-card"; + const scopeSelector = `[${scopeAttr}]`; + const dangerCardCss = ` +${scopeSelector} { + border-color: #fecaca; + background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); +} + +${scopeSelector} .org-danger-copy { + margin-bottom: 1rem; +} + +${scopeSelector} .org-danger-copy strong, +${scopeSelector} .org-danger-copy p { + display: block; +} + +${scopeSelector} .org-danger-copy p { + margin: 0.4rem 0 0; + color: var(--text-muted); +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DangerCard = function DangerCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + ensureScopedStyle("portal-danger-card", dangerCardCss); + + if (!permissions.canDisbandOrg()) { + return null; + } + + return PanelCard({ + className: "org-span-12 org-danger-panel", + title: "Organization Controls", + subtitle: + "Leader-only actions for membership and permanent organization removal.", + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + null, + h( + "div", + { className: "org-danger-copy" }, + h("strong", null, "Disband organization"), + h( + "p", + null, + "This removes the organization and revokes access to the portal for all members.", + ), + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.openModal("disband"), + }, + "Disband Organization", + ), + ), + }); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/fleetCard.js b/arma/client/addons/org/ui/_site/components/portal/fleetCard.js new file mode 100644 index 0000000..6869901 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/fleetCard.js @@ -0,0 +1,101 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const actions = OrgPortal.actions; + const scopeAttr = "data-ui-fleet-card"; + const scopeSelector = `[${scopeAttr}]`; + const fleetCardCss = ` +${scopeSelector} .org-simple-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +${scopeSelector} { + min-height: 32.5rem; + max-height: 32.5rem; +} + +${scopeSelector} .org-simple-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-simple-row:nth-child(even) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(148 163 184 / 0.45); +} + +${scopeSelector} .org-simple-name { + color: var(--primary-hover); +} + +${scopeSelector} .org-simple-meta { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 1rem; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-simple-row { + flex-direction: column; + align-items: flex-start; + } +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FleetCard = function FleetCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + const SimpleStat = OrgPortal.componentFns.SimpleStat; + ensureScopedStyle("portal-fleet-card", fleetCardCss); + + return PanelCard({ + className: "org-scroll-panel org-span-7", + title: "Fleet", + subtitle: + "Individual vehicles with type, status, and overall damage.", + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + { className: "org-simple-list" }, + ...portalData.fleet.map((unit) => + h( + "article", + { className: "org-simple-row" }, + h( + "strong", + { className: "org-simple-name" }, + unit.name, + ), + h( + "div", + { className: "org-simple-meta" }, + SimpleStat( + "Type", + actions.formatVehicleType(unit.type), + ), + SimpleStat("Status", unit.status), + SimpleStat("Damage", unit.damage), + ), + ), + ), + ), + }); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/futureCard.js b/arma/client/addons/org/ui/_site/components/portal/futureCard.js new file mode 100644 index 0000000..d69077d --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/futureCard.js @@ -0,0 +1,105 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const scopeAttr = "data-ui-future-card"; + const scopeSelector = `[${scopeAttr}]`; + const futureCardCss = ` +${scopeSelector} .org-roadmap-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + flex: 1; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +${scopeSelector} .org-roadmap-card { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.7rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-roadmap-card:nth-child(4n + 2), +${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(100 116 139 / 0.4); +} + +${scopeSelector} .org-roadmap-card p { + margin: 0; + color: var(--text-main); +} + +${scopeSelector} .org-list-tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + background: #e2e8f0; + color: var(--primary-hover); +} + +${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag, +${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { + background: #cbd5e1; + color: #1e293b; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-roadmap-grid { + grid-template-columns: 1fr; + } + + ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { + background: #f8fafc; + border-color: var(--border); + } + + ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { + background: #e2e8f0; + color: var(--primary-hover); + } +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.FutureCard = function FutureCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + ensureScopedStyle("portal-future-card", futureCardCss); + + return PanelCard({ + className: "org-scroll-panel org-span-6", + title: "Expansion Slots", + subtitle: + "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + { className: "org-roadmap-grid" }, + ...portalData.roadmap.map((item) => + h( + "article", + { className: "org-roadmap-card" }, + h("span", { className: "org-list-tag" }, item.status), + h("strong", null, item.name), + h("p", null, item.detail), + ), + ), + ), + }); + }; +})(); diff --git a/arma/ui/apps/portal/components/membersCard.js b/arma/client/addons/org/ui/_site/components/portal/membersCard.js similarity index 56% rename from arma/ui/apps/portal/components/membersCard.js rename to arma/client/addons/org/ui/_site/components/portal/membersCard.js index 8333b7c..3bc616e 100644 --- a/arma/ui/apps/portal/components/membersCard.js +++ b/arma/client/addons/org/ui/_site/components/portal/membersCard.js @@ -1,42 +1,83 @@ (function () { const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; + const { h, ensureScopedStyle } = OrgPortal.runtime; const store = OrgPortal.store; const permissions = OrgPortal.permissions; const actions = OrgPortal.actions; + const scopeAttr = "data-ui-members-card"; + const scopeSelector = `[${scopeAttr}]`; + const membersCardCss = ` +${scopeSelector} .org-name-list { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.85rem; + min-height: 0; + overflow: auto; + padding-right: 0.35rem; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +${scopeSelector} .org-name-row { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-name-row:nth-child(even) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(148 163 184 / 0.45); +} + +${scopeSelector} .org-name-row button { + margin-left: auto; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-name-row { + flex-direction: column; + align-items: flex-start; + } + + ${scopeSelector} .org-name-row button { + margin-left: 0; + } +} +`; OrgPortal.componentFns = OrgPortal.componentFns || {}; OrgPortal.componentFns.MembersCard = function MembersCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; const members = store.getMembers(); const allowMemberManagement = permissions.canManageMembers(); + ensureScopedStyle("portal-members-card", membersCardCss); - return h( - "section", - { className: "card org-panel org-scroll-panel org-span-5" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, "Members"), - h( - "p", - { className: "org-panel-subtitle" }, - "Current roster listing with member removal controls.", - ), - ), - ), - h( + return PanelCard({ + className: "org-scroll-panel org-span-5", + title: "Members", + subtitle: + "Current roster listing. The organization owner cannot be removed.", + rootProps: { [scopeAttr]: "" }, + body: h( "div", { className: "org-name-list" }, - ...members.map((member) => - h( + ...members.map((member) => { + const canRemoveMember = + allowMemberManagement && + !actions.isOwnerMember(member.name); + + return h( "article", { className: "org-name-row" }, h("strong", null, member.name), - allowMemberManagement + canRemoveMember ? h( "button", { @@ -67,9 +108,9 @@ ), ) : null, - ), - ), + ); + }), ), - ); + }); }; })(); diff --git a/arma/client/addons/org/ui/_site/components/portal/metricCard.js b/arma/client/addons/org/ui/_site/components/portal/metricCard.js new file mode 100644 index 0000000..ed0d7a3 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/metricCard.js @@ -0,0 +1,77 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const scopeAttr = "data-ui-metric-card"; + const scopeSelector = `[${scopeAttr}]`; + const metricCardCss = ` +${scopeSelector} { + display: flex; + flex-direction: column; + gap: 0.45rem; + padding: 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +${scopeSelector}:nth-child(4n + 2), +${scopeSelector}:nth-child(4n + 3) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%); + border-color: rgb(100 116 139 / 0.35); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); +} + +${scopeSelector} .org-metric-label { + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +${scopeSelector} .org-metric-value { + font-size: 1.8rem; + color: var(--primary-hover); + line-height: 1.1; +} + +${scopeSelector}:nth-child(4n + 2) .org-metric-value, +${scopeSelector}:nth-child(4n + 3) .org-metric-value { + color: #334155; +} + +${scopeSelector} .org-metric-note { + color: var(--text-muted); + font-size: 0.9rem; +} + +@media (max-width: 960px) { + ${scopeSelector}:nth-child(4n + 3) { + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + border-color: var(--border); + box-shadow: none; + } + + ${scopeSelector}:nth-child(4n + 3) .org-metric-value { + color: var(--primary-hover); + } +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.MetricCard = function MetricCard( + label, + value, + note, + ) { + ensureScopedStyle("portal-metric-card", metricCardCss); + + return h( + "div", + { className: "org-metric-card", [scopeAttr]: "" }, + h("span", { className: "org-metric-label" }, label), + h("strong", { className: "org-metric-value" }, value), + h("span", { className: "org-metric-note" }, note), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/modalLayer.js b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js similarity index 85% rename from arma/client/addons/org/ui/_site/portal/components/modalLayer.js rename to arma/client/addons/org/ui/_site/components/portal/modalLayer.js index af566ad..a0f0d3b 100644 --- a/arma/client/addons/org/ui/_site/portal/components/modalLayer.js +++ b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js @@ -8,6 +8,7 @@ OrgPortal.componentFns = OrgPortal.componentFns || {}; OrgPortal.componentFns.ModalLayer = function ModalLayer() { + const Modal = window.SharedUI.componentFns.Modal; const modal = store.getModal(); if (!modal) { return null; @@ -24,7 +25,7 @@ title = "Run Payroll"; body = h( "div", - { className: "org-modal-form" }, + { className: "app-modal-form" }, h( "div", null, @@ -39,7 +40,7 @@ ), h( "div", - { className: "org-modal-actions" }, + { className: "app-modal-actions" }, h( "button", { @@ -75,7 +76,7 @@ title = "Send Funds"; body = h( "div", - { className: "org-modal-form" }, + { className: "app-modal-form" }, h( "div", null, @@ -104,7 +105,7 @@ ), h( "div", - { className: "org-modal-actions" }, + { className: "app-modal-actions" }, h( "button", { @@ -146,7 +147,7 @@ title = "Assign Credit Line"; body = h( "div", - { className: "org-modal-form" }, + { className: "app-modal-form" }, h( "div", null, @@ -172,7 +173,7 @@ ), h( "div", - { className: "org-modal-actions" }, + { className: "app-modal-actions" }, h( "button", { @@ -214,7 +215,7 @@ title = "Disband Organization"; body = h( "div", - { className: "org-danger-confirm" }, + { className: "app-modal-danger" }, h( "p", null, @@ -224,7 +225,7 @@ ), h( "div", - { className: "org-danger-actions" }, + { className: "app-modal-danger-actions" }, h( "button", { @@ -247,40 +248,10 @@ ); } - return h( - "div", - { - className: "org-modal-backdrop", - onClick: (e) => { - if (e.target === e.currentTarget) { - actions.closeModal(); - } - }, - }, - h( - "div", - { className: "card org-modal-card" }, - h( - "div", - { className: "org-modal-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, title), - ), - h( - "button", - { - type: "button", - className: "org-modal-close", - onClick: () => actions.closeModal(), - "aria-label": "Close dialog", - }, - "x", - ), - ), - body, - ), - ); + return Modal({ + title, + body, + onClose: () => actions.closeModal(), + }); }; })(); diff --git a/arma/ui/apps/portal/components/overviewCard.js b/arma/client/addons/org/ui/_site/components/portal/overviewCard.js similarity index 67% rename from arma/ui/apps/portal/components/overviewCard.js rename to arma/client/addons/org/ui/_site/components/portal/overviewCard.js index 658f895..87fada3 100644 --- a/arma/ui/apps/portal/components/overviewCard.js +++ b/arma/client/addons/org/ui/_site/components/portal/overviewCard.js @@ -1,35 +1,90 @@ (function () { const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; + const { h, ensureScopedStyle } = OrgPortal.runtime; const { portalData } = OrgPortal.data; const store = OrgPortal.store; const actions = OrgPortal.actions; + const scopeAttr = "data-ui-overview-card"; + const scopeSelector = `[${scopeAttr}]`; + const overviewCardCss = ` +${scopeSelector} .org-hero-grid { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 1.5rem; + align-items: start; +} + +${scopeSelector} .org-summary { + margin: 0; + font-size: 1.05rem; + color: var(--text-main); +} + +${scopeSelector} .org-meta-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +${scopeSelector} .org-meta-item { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-meta-item:nth-child(even) { + background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%); + border-color: rgb(148 163 184 / 0.45); +} + +${scopeSelector} .org-meta-label { + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +${scopeSelector} .org-meta-value { + font-size: 1rem; + font-weight: 600; + color: var(--primary-hover); +} + +${scopeSelector} .org-metric-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +@media (max-width: 960px) { + ${scopeSelector} .org-hero-grid, + ${scopeSelector} .org-meta-row, + ${scopeSelector} .org-metric-grid { + grid-template-columns: 1fr; + } +} +`; OrgPortal.componentFns = OrgPortal.componentFns || {}; OrgPortal.componentFns.OverviewCard = function OverviewCard() { const MetricCard = OrgPortal.componentFns.MetricCard; + const PanelCard = window.SharedUI.componentFns.PanelCard; const readiness = actions.getAssetReadiness(); const headquarters = portalData.org.headquarters || "ArmA Verse"; + ensureScopedStyle("portal-overview-card", overviewCardCss); - return h( - "section", - { className: "card org-panel org-span-12" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("div", { className: "org-eyebrow" }, portalData.org.tag), - h( - "h2", - { className: "org-panel-title" }, - "Organization Overview", - ), - ), - ), - h( + return PanelCard({ + className: "org-span-12", + eyebrow: portalData.org.tag, + title: "Organization Overview", + rootProps: { [scopeAttr]: "" }, + body: h( "div", { className: "org-hero-grid" }, h( @@ -115,6 +170,6 @@ ), ), ), - ); + }); }; })(); diff --git a/arma/client/addons/org/ui/_site/components/portal/simpleStat.js b/arma/client/addons/org/ui/_site/components/portal/simpleStat.js new file mode 100644 index 0000000..eceb6f6 --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/simpleStat.js @@ -0,0 +1,39 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const scopeAttr = "data-ui-simple-stat"; + const scopeSelector = `[${scopeAttr}]`; + const simpleStatCss = ` +${scopeSelector} { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 90px; +} + +${scopeSelector} .org-simple-label { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +${scopeSelector} .org-simple-value { + font-size: 0.95rem; + color: var(--text-main); +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { + ensureScopedStyle("portal-simple-stat", simpleStatCss); + + return h( + "div", + { className: "org-simple-stat", [scopeAttr]: "" }, + h("span", { className: "org-simple-label" }, label), + h("strong", { className: "org-simple-value" }, value), + ); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/components/portal/treasuryCard.js b/arma/client/addons/org/ui/_site/components/portal/treasuryCard.js new file mode 100644 index 0000000..b43549b --- /dev/null +++ b/arma/client/addons/org/ui/_site/components/portal/treasuryCard.js @@ -0,0 +1,430 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const store = OrgPortal.store; + const permissions = OrgPortal.permissions; + const actions = OrgPortal.actions; + const scopeAttr = "data-ui-treasury-card"; + const scopeSelector = `[${scopeAttr}]`; + const [getTreasuryTab, setTreasuryTab] = createSignal("overview"); + const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false); + const treasuryCardCss = ` +${scopeSelector} .org-treasury-menu { + position: relative; +} + +${scopeSelector} .org-menu-btn { + width: 2.75rem; + height: 2.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--border); + background: #f8fafc; + color: var(--text-muted); +} + +${scopeSelector} .org-menu-btn:hover { + color: var(--primary-hover); + border-color: rgb(148 163 184 / 0.65); +} + +${scopeSelector} .org-menu-btn svg { + width: 1.1rem; + height: 1.1rem; +} + +${scopeSelector} .org-menu-dropdown { + position: absolute; + top: calc(100% + 0.6rem); + right: 0; + min-width: 10.5rem; + padding: 0.45rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #fff; + box-shadow: 0 12px 28px rgb(15 23 42 / 0.12); + display: flex; + flex-direction: column; + gap: 0.35rem; + z-index: 5; +} + +${scopeSelector} .org-menu-option + .org-menu-option { + margin-left: 0; +} + +${scopeSelector} .org-menu-option { + width: 100%; + justify-content: flex-start; + background: transparent; + color: var(--text-main); + border: 1px solid transparent; +} + +${scopeSelector} .org-menu-option:hover { + background: #f8fafc; + border-color: rgb(148 163 184 / 0.35); +} + +${scopeSelector} .org-menu-option.is-active { + background: rgb(226 232 240 / 0.7); + color: var(--primary-hover); + border-color: rgb(148 163 184 / 0.35); +} + +${scopeSelector} .org-finance-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +${scopeSelector} .org-finance-meta > div { + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +${scopeSelector} .org-meta-label { + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +${scopeSelector} .org-action-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; +} + +${scopeSelector} .org-action-grid button + button { + margin-left: 0; +} + +${scopeSelector} .org-action-grid button { + width: 100%; +} + +${scopeSelector} .org-access-note { + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.95rem; +} + +${scopeSelector} .org-credit-summary { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.85rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-credit-summary strong { + font-size: 1rem; +} + +${scopeSelector} .org-credit-summary span:last-child { + font-size: 0.92rem; + line-height: 1.45; +} + +${scopeSelector} .org-credit-lines-list { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +${scopeSelector} .org-credit-line-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +${scopeSelector} .org-credit-line-row:nth-child(even) { + background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); + border-color: rgb(148 163 184 / 0.45); +} + +${scopeSelector} .org-credit-line-member { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +${scopeSelector} .org-credit-line-label { + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +${scopeSelector} .org-credit-line-empty { + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; + color: var(--text-muted); +} + +@media (max-width: 960px) { + ${scopeSelector} .org-finance-meta { + grid-template-columns: 1fr; + } + + ${scopeSelector} .org-credit-line-row { + flex-direction: column; + align-items: flex-start; + } +} +`; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + const creditLines = store.getCreditLines(); + const allowTreasuryActions = permissions.canManageTreasury(); + const activeTab = getTreasuryTab(); + const isMenuOpen = getTreasuryMenuOpen(); + const activeCreditLabel = + creditLines.length === 1 + ? "1 active credit line" + : `${creditLines.length} active credit lines`; + ensureScopedStyle("portal-treasury-card", treasuryCardCss); + + return PanelCard({ + className: "org-span-5", + title: "Treasury", + subtitle: "Organization funds, reputation, and member payouts.", + headerExtras: h( + "div", + { className: "org-treasury-menu" }, + h( + "button", + { + type: "button", + className: "org-menu-btn", + title: "Treasury views", + "aria-label": "Treasury views", + onClick: () => setTreasuryMenuOpen((open) => !open), + }, + h( + "svg", + { + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "aria-hidden": "true", + }, + h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }), + h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }), + h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }), + ), + ), + isMenuOpen + ? h( + "div", + { className: "org-menu-dropdown" }, + h( + "button", + { + type: "button", + className: + activeTab === "overview" + ? "org-menu-option is-active" + : "org-menu-option", + onClick: () => { + setTreasuryTab("overview"); + setTreasuryMenuOpen(false); + }, + }, + "Overview", + ), + h( + "button", + { + type: "button", + className: + activeTab === "credit" + ? "org-menu-option is-active" + : "org-menu-option", + onClick: () => { + setTreasuryTab("credit"); + setTreasuryMenuOpen(false); + }, + }, + "Credit Lines", + ), + ) + : null, + ), + rootProps: { [scopeAttr]: "" }, + body: h( + "div", + null, + activeTab === "credit" + ? creditLines.length > 0 + ? h( + "div", + { className: "org-credit-lines-list" }, + ...creditLines.map((line) => + h( + "article", + { className: "org-credit-line-row" }, + h( + "div", + { + className: + "org-credit-line-member", + }, + h( + "span", + { + className: + "org-credit-line-label", + }, + "Member", + ), + h("strong", null, line.member), + ), + h( + "div", + { + className: + "org-credit-line-member", + }, + h( + "span", + { + className: + "org-credit-line-label", + }, + "Amount", + ), + h( + "strong", + null, + actions.formatCurrency( + line.amount, + ), + ), + ), + ), + ), + ) + : h( + "div", + { className: "org-credit-line-empty" }, + "No active credit lines.", + ) + : h( + "div", + null, + h( + "div", + { className: "org-finance-meta" }, + h( + "div", + null, + h( + "span", + { className: "org-meta-label" }, + "Funds", + ), + h( + "strong", + null, + actions.formatCurrency(store.getFunds()), + ), + ), + h( + "div", + null, + h( + "span", + { className: "org-meta-label" }, + "Reputation", + ), + h("strong", null, `${portalData.reputation}`), + ), + ), + allowTreasuryActions + ? h( + "div", + { className: "org-action-grid" }, + h( + "button", + { + type: "button", + onClick: () => + actions.openModal("payroll"), + }, + "Run Payroll", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => + actions.openModal("transfer"), + }, + "Send Funds", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => + actions.openModal("credit"), + }, + "Credit Line", + ), + ) + : h( + "p", + { className: "org-access-note" }, + "Only the organization leader or CEO can manage treasury actions.", + ), + h( + "div", + { className: "org-credit-summary" }, + h( + "span", + { className: "org-meta-label" }, + "Credit Line Status", + ), + h("strong", null, activeCreditLabel), + h( + "span", + null, + creditLines.length > 0 + ? "Open the Credit Lines tab to review assigned members and amounts." + : "Assign a credit line to create the first approved member limit.", + ), + ), + ), + ), + }); + }; +})(); diff --git a/arma/client/addons/org/ui/_site/portal/components/controls.css b/arma/client/addons/org/ui/_site/controls.css similarity index 100% rename from arma/client/addons/org/ui/_site/portal/components/controls.css rename to arma/client/addons/org/ui/_site/controls.css diff --git a/arma/client/addons/org/ui/_site/portal/components/portalHeader.css b/arma/client/addons/org/ui/_site/hero.css similarity index 100% rename from arma/client/addons/org/ui/_site/portal/components/portalHeader.css rename to arma/client/addons/org/ui/_site/hero.css diff --git a/arma/client/addons/org/ui/_site/index.html b/arma/client/addons/org/ui/_site/index.html index b3e5698..ec7f3c4 100644 --- a/arma/client/addons/org/ui/_site/index.html +++ b/arma/client/addons/org/ui/_site/index.html @@ -6,55 +6,41 @@ ORBIS - Global Organization Network + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/arma/ui/apps/main/state.js b/arma/ui/apps/main/state.js index d293133..c4c0106 100644 --- a/arma/ui/apps/main/state.js +++ b/arma/ui/apps/main/state.js @@ -1,43 +1,23 @@ (function () { const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); const { createSignal } = RegistryApp.runtime; + const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - class RegistryStore { - constructor() { - [this.getView, this.setView] = createSignal("home"); - [this.getIsAuthenticating, this.setIsAuthenticating] = - createSignal(false); - [this.getLoginError, this.setLoginError] = createSignal(""); - } - - startLogin() { - this.setLoginError(""); - this.setIsAuthenticating(true); - } - - failLogin(message) { - this.setIsAuthenticating(false); - this.setLoginError(message || "Authentication failed."); - } - - completeLogin(payload) { + RegistryApp.store = SharedLogic.createRegistryStore({ + createSignal, + onHydratePortal(payload) { const OrgPortal = window.OrgPortal; const portalData = payload?.portalData; const session = payload?.session; if (!OrgPortal || !portalData || !session) { - this.failLogin("Login response was missing portal data."); - return; + return false; } OrgPortal.data.applyLoginPayload(payload); OrgPortal.store.hydrateFromPayload(payload); - - this.setLoginError(""); - this.setIsAuthenticating(false); - this.setView("portal"); - } - } - - RegistryApp.store = new RegistryStore(); + RegistryApp.store.setView("portal"); + return true; + }, + }); })(); diff --git a/arma/ui/apps/portal/actions.js b/arma/ui/apps/portal/actions.js index 85c3b1c..b8b3f5e 100644 --- a/arma/ui/apps/portal/actions.js +++ b/arma/ui/apps/portal/actions.js @@ -4,289 +4,12 @@ const store = OrgPortal.store; const permissions = OrgPortal.permissions; const registryStore = window.RegistryApp.store; + const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - class OrgPortalActions { - constructor() { - this.treasuryNoticeTimer = null; - } - - formatCurrency(value) { - return "$" + value.toLocaleString(); - } - - formatVehicleType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatAssetType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatDisplayName(value) { - if (!value) { - return ""; - } - - return String(value) - .trim() - .split(/\s+/) - .map((part) => { - if (!part) { - return ""; - } - - return ( - part.charAt(0).toUpperCase() + - part.slice(1).toLowerCase() - ); - }) - .join(" "); - } - - getAssetReadiness() { - if (portalData.fleet.length === 0) { - return null; - } - - const total = portalData.fleet.reduce( - (sum, unit) => sum + (100 - parseInt(unit.damage, 10)), - 0, - ); - return Math.round(total / portalData.fleet.length); - } - - showTreasuryNotice(type, text) { - store.setTreasuryNotice({ type, text }); - - if (this.treasuryNoticeTimer) { - clearTimeout(this.treasuryNoticeTimer); - } - - this.treasuryNoticeTimer = setTimeout(() => { - store.setTreasuryNotice({ type: "", text: "" }); - this.treasuryNoticeTimer = null; - }, 3500); - } - - parseAmount(value) { - const amount = Number(value); - return Number.isFinite(amount) ? Math.round(amount) : 0; - } - - getInputValue(id) { - const el = document.getElementById(id); - return el ? el.value : ""; - } - - closePortal() { - if ( - typeof A3API !== "undefined" && - typeof A3API.SendAlert === "function" - ) { - A3API.SendAlert( - JSON.stringify({ - event: "org::close", - data: {}, - }), - ); - return; - } - - if (registryStore) { - registryStore.setView("home"); - } - } - - openModal(type) { - if ( - (type === "payroll" || - type === "transfer" || - type === "credit") && - !permissions.canManageTreasury() - ) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return; - } - - if (type === "disband" && !permissions.canDisbandOrg()) { - return; - } - - store.setModal({ type }); - } - - closeModal() { - store.setModal(null); - } - - removeMember(memberName) { - if (!permissions.canManageMembers()) { - return false; - } - - store.setMembers((currentMembers) => - currentMembers.filter((member) => member.name !== memberName), - ); - store.setCreditLines((currentLines) => - currentLines.filter((line) => line.member !== memberName), - ); - return true; - } - - disbandOrganization() { - if (!permissions.canDisbandOrg()) { - return false; - } - - store.setOrgDisbanded(true); - this.closeModal(); - return true; - } - - runPayroll(amountPerMember) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - const members = store.getMembers(); - const funds = store.getFunds(); - - if (members.length === 0) { - this.showTreasuryNotice( - "error", - "No members available for payroll.", - ); - return false; - } - - if (amountPerMember <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid payroll amount.", - ); - return false; - } - - const total = amountPerMember * members.length; - if (total > funds) { - this.showTreasuryNotice( - "error", - "Insufficient org funds for payroll.", - ); - return false; - } - - store.setFunds(funds - total); - this.showTreasuryNotice( - "success", - `Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`, - ); - return true; - } - - sendFundsToMember(memberName, amount) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - const funds = store.getFunds(); - - if (!memberName) { - this.showTreasuryNotice( - "error", - "Select a member to receive funds.", - ); - return false; - } - - if (amount <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid transfer amount.", - ); - return false; - } - - if (amount > funds) { - this.showTreasuryNotice( - "error", - "Insufficient org funds for this transfer.", - ); - return false; - } - - store.setFunds(funds - amount); - this.showTreasuryNotice( - "success", - `${this.formatCurrency(amount)} sent to ${memberName}.`, - ); - return true; - } - - grantCreditLine(memberName, amount) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - if (!memberName) { - this.showTreasuryNotice( - "error", - "Select a member for the credit line.", - ); - return false; - } - - if (amount <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid credit line amount.", - ); - return false; - } - - store.setCreditLines((currentLines) => { - const existingIndex = currentLines.findIndex( - (line) => line.member === memberName, - ); - if (existingIndex === -1) { - return [...currentLines, { member: memberName, amount }]; - } - - const updatedLines = [...currentLines]; - updatedLines[existingIndex] = { member: memberName, amount }; - return updatedLines; - }); - - this.showTreasuryNotice( - "success", - `Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`, - ); - return true; - } - } - - OrgPortal.actions = new OrgPortalActions(); + OrgPortal.actions = SharedLogic.createPortalActions({ + portalData, + store, + permissions, + registryStore, + }); })(); diff --git a/arma/ui/apps/portal/components/activityCard.css b/arma/ui/apps/portal/components/activityCard.css deleted file mode 100644 index d26dbf9..0000000 --- a/arma/ui/apps/portal/components/activityCard.css +++ /dev/null @@ -1,32 +0,0 @@ -.org-activity-row { - padding: 1rem; - border: 1px solid var(--border); - border-left: 3px solid #94a3b8; - border-radius: var(--radius); - background: #f8fafc; - - &:nth-child(even) { - background: linear-gradient( - 180deg, - rgb(248 250 252) 0%, - rgb(241 245 249) 100% - ); - border-color: rgb(148 163 184 / 0.45); - border-left-color: #64748b; - } - - p { - margin: 0; - color: var(--text-main); - } -} - -.org-activity-time { - display: inline-block; - margin-bottom: 0.35rem; - color: var(--text-muted); - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.05em; - text-transform: uppercase; -} diff --git a/arma/ui/apps/portal/components/activityCard.js b/arma/ui/apps/portal/components/activityCard.js deleted file mode 100644 index 0de0ea8..0000000 --- a/arma/ui/apps/portal/components/activityCard.js +++ /dev/null @@ -1,44 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.ActivityCard = function ActivityCard() { - return h( - "section", - { className: "card org-panel org-scroll-panel org-span-6" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, "Command Feed"), - h( - "p", - { className: "org-panel-subtitle" }, - "Recent organization-level actions and updates.", - ), - ), - ), - h( - "div", - { className: "org-activity-list" }, - ...portalData.activity.map((item) => - h( - "article", - { className: "org-activity-row" }, - h( - "span", - { className: "org-activity-time" }, - item.time, - ), - h("p", null, item.text), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/assetsCard.js b/arma/ui/apps/portal/components/assetsCard.js deleted file mode 100644 index 86473fc..0000000 --- a/arma/ui/apps/portal/components/assetsCard.js +++ /dev/null @@ -1,55 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const actions = OrgPortal.actions; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.AssetsCard = function AssetsCard() { - const SimpleStat = OrgPortal.componentFns.SimpleStat; - - return h( - "section", - { className: "card org-panel org-scroll-panel org-span-7" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, "Assets"), - h( - "p", - { className: "org-panel-subtitle" }, - "Inventory supplies and equipment with quantity totals.", - ), - ), - ), - h( - "div", - { className: "org-simple-list" }, - ...portalData.assets.map((asset) => - h( - "article", - { className: "org-simple-row" }, - h( - "strong", - { className: "org-simple-name" }, - asset.name, - ), - h( - "div", - { className: "org-simple-meta" }, - SimpleStat( - "Type", - actions.formatAssetType(asset.type), - ), - SimpleStat("Quantity", asset.quantity), - ), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/dangerCard.css b/arma/ui/apps/portal/components/dangerCard.css deleted file mode 100644 index 82a8d12..0000000 --- a/arma/ui/apps/portal/components/dangerCard.css +++ /dev/null @@ -1,22 +0,0 @@ -.org-danger-panel { - border-color: #fecaca; - background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); -} - -.org-danger-copy { - margin-bottom: 1rem; - - strong, - p { - display: block; - } - - p { - margin: 0.4rem 0 0; - color: var(--text-muted); - } -} - -.org-empty-state { - text-align: left; -} diff --git a/arma/ui/apps/portal/components/dangerCard.js b/arma/ui/apps/portal/components/dangerCard.js deleted file mode 100644 index 16fbf31..0000000 --- a/arma/ui/apps/portal/components/dangerCard.js +++ /dev/null @@ -1,56 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const permissions = OrgPortal.permissions; - const actions = OrgPortal.actions; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DangerCard = function DangerCard() { - if (!permissions.canDisbandOrg()) { - return null; - } - - return h( - "section", - { className: "card org-panel org-span-12 org-danger-panel" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h( - "h2", - { className: "org-panel-title" }, - "Organization Controls", - ), - h( - "p", - { className: "org-panel-subtitle" }, - "Leader-only actions for membership and permanent organization removal.", - ), - ), - ), - h( - "div", - { className: "org-danger-copy" }, - h("strong", null, "Disband organization"), - h( - "p", - null, - "This removes the organization and revokes access to the portal for all members.", - ), - ), - h( - "button", - { - type: "button", - className: "org-danger-btn", - onClick: () => actions.openModal("disband"), - }, - "Disband Organization", - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/disbandedView.js b/arma/ui/apps/portal/components/disbandedView.js deleted file mode 100644 index 94d4497..0000000 --- a/arma/ui/apps/portal/components/disbandedView.js +++ /dev/null @@ -1,47 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const registryStore = window.RegistryApp.store; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DisbandedView = function DisbandedView() { - return h( - "section", - { className: "card org-panel org-span-12 org-empty-state" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h( - "div", - { className: "org-eyebrow" }, - "Organization Removed", - ), - h( - "h2", - { className: "org-panel-title" }, - portalData.org.name, - ), - ), - ), - h( - "p", - { className: "org-summary" }, - "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => registryStore.setView("home"), - }, - "Return to Registry", - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/fleetCard.js b/arma/ui/apps/portal/components/fleetCard.js deleted file mode 100644 index 7e69f37..0000000 --- a/arma/ui/apps/portal/components/fleetCard.js +++ /dev/null @@ -1,56 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const actions = OrgPortal.actions; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FleetCard = function FleetCard() { - const SimpleStat = OrgPortal.componentFns.SimpleStat; - - return h( - "section", - { className: "card org-panel org-scroll-panel org-span-7" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, "Fleet"), - h( - "p", - { className: "org-panel-subtitle" }, - "Individual vehicles with type, status, and overall damage.", - ), - ), - ), - h( - "div", - { className: "org-simple-list" }, - ...portalData.fleet.map((unit) => - h( - "article", - { className: "org-simple-row" }, - h( - "strong", - { className: "org-simple-name" }, - unit.name, - ), - h( - "div", - { className: "org-simple-meta" }, - SimpleStat( - "Type", - actions.formatVehicleType(unit.type), - ), - SimpleStat("Status", unit.status), - SimpleStat("Damage", unit.damage), - ), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/footer.js b/arma/ui/apps/portal/components/footer.js deleted file mode 100644 index 50454df..0000000 --- a/arma/ui/apps/portal/components/footer.js +++ /dev/null @@ -1,43 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.Footer = function Footer() { - return h( - "div", - { className: "footer" }, - h( - "div", - { className: "wrapper" }, - h( - "div", - null, - h("h3", null, "Organization Controls"), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "Roster Management"), - h("li", null, "Fleet Assignment"), - h("li", null, "Treasury Permissions"), - h("li", null, "Asset Registry"), - ), - ), - h( - "div", - null, - h("h3", null, "Planned Extensions"), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "Contracts Board"), - h("li", null, "Diplomacy Layer"), - h("li", null, "Procurement Queue"), - h("li", null, "Reputation History"), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/futureCard.css b/arma/ui/apps/portal/components/futureCard.css deleted file mode 100644 index ad6407e..0000000 --- a/arma/ui/apps/portal/components/futureCard.css +++ /dev/null @@ -1,74 +0,0 @@ -.org-roadmap-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - flex: 1; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; -} - -.org-roadmap-card { - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.7rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - - &:nth-child(4n + 2), - &:nth-child(4n + 3) { - background: linear-gradient( - 180deg, - rgb(248 250 252) 0%, - rgb(241 245 249) 100% - ); - border-color: rgb(100 116 139 / 0.4); - } - - p { - margin: 0; - color: var(--text-main); - } -} - -.org-list-tag { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.2rem 0.55rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - background: #e2e8f0; - color: var(--primary-hover); - - .org-roadmap-card:nth-child(4n + 2) &, - .org-roadmap-card:nth-child(4n + 3) & { - background: #cbd5e1; - color: #1e293b; - } -} - -@media (max-width: 960px) { - .org-roadmap-grid { - grid-template-columns: 1fr; - } - - .org-roadmap-card { - &:nth-child(4n + 3) { - background: #f8fafc; - border-color: var(--border); - } - } - - .org-list-tag { - .org-roadmap-card:nth-child(4n + 3) & { - background: #e2e8f0; - color: var(--primary-hover); - } - } -} diff --git a/arma/ui/apps/portal/components/futureCard.js b/arma/ui/apps/portal/components/futureCard.js deleted file mode 100644 index 72b7158..0000000 --- a/arma/ui/apps/portal/components/futureCard.js +++ /dev/null @@ -1,45 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FutureCard = function FutureCard() { - return h( - "section", - { className: "card org-panel org-scroll-panel org-span-6" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h( - "h2", - { className: "org-panel-title" }, - "Expansion Slots", - ), - h( - "p", - { className: "org-panel-subtitle" }, - "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", - ), - ), - ), - h( - "div", - { className: "org-roadmap-grid" }, - ...portalData.roadmap.map((item) => - h( - "article", - { className: "org-roadmap-card" }, - h("span", { className: "org-list-tag" }, item.status), - h("strong", null, item.name), - h("p", null, item.detail), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/index.js b/arma/ui/apps/portal/components/index.js deleted file mode 100644 index d6a80e9..0000000 --- a/arma/ui/apps/portal/components/index.js +++ /dev/null @@ -1,65 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const store = OrgPortal.store; - - OrgPortal.components = OrgPortal.components || {}; - - OrgPortal.components.App = function App() { - const PortalHeader = OrgPortal.componentFns.PortalHeader; - const OverviewCard = OrgPortal.componentFns.OverviewCard; - const FleetCard = OrgPortal.componentFns.FleetCard; - const TreasuryCard = OrgPortal.componentFns.TreasuryCard; - const MembersCard = OrgPortal.componentFns.MembersCard; - const AssetsCard = OrgPortal.componentFns.AssetsCard; - const ActivityCard = OrgPortal.componentFns.ActivityCard; - const FutureCard = OrgPortal.componentFns.FutureCard; - const DangerCard = OrgPortal.componentFns.DangerCard; - const ModalLayer = OrgPortal.componentFns.ModalLayer; - const DisbandedView = OrgPortal.componentFns.DisbandedView; - const Footer = OrgPortal.componentFns.Footer; - - if (store.getOrgDisbanded()) { - return h( - "main", - null, - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - PortalHeader(), - DisbandedView(), - ), - ), - ModalLayer(), - Footer(), - ); - } - - return h( - "main", - null, - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - PortalHeader(), - OverviewCard(), - FleetCard(), - TreasuryCard(), - MembersCard(), - AssetsCard(), - ActivityCard(), - FutureCard(), - DangerCard(), - ), - ), - ModalLayer(), - Footer(), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/layout.css b/arma/ui/apps/portal/components/layout.css deleted file mode 100644 index b46b675..0000000 --- a/arma/ui/apps/portal/components/layout.css +++ /dev/null @@ -1,81 +0,0 @@ -.org-dashboard-grid { - display: grid; - grid-template-columns: repeat(12, minmax(0, 1fr)); - gap: 1.5rem; -} - -.org-panel { - margin-bottom: 0; - text-align: left; -} - -.org-scroll-panel { - display: flex; - flex-direction: column; - max-height: 31rem; - overflow: hidden; -} - -.org-span-12 { - grid-column: span 12; -} - -.org-span-7 { - grid-column: span 7; -} - -.org-span-6 { - grid-column: span 6; -} - -.org-span-5 { - grid-column: span 5; -} - -.org-panel-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1.5rem; - - &.org-panel-head-stack { - flex-direction: column; - align-items: stretch; - } -} - -.org-eyebrow { - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--text-muted); - margin-bottom: 0.4rem; -} - -.org-panel-title { - margin: 0; - color: var(--primary-hover); - font-size: 1.45rem; -} - -.org-panel-subtitle { - margin: 0.35rem 0 0; - color: var(--text-muted); - font-size: 0.95rem; -} - -@media (max-width: 960px) { - .org-span-12, - .org-span-7, - .org-span-6, - .org-span-5 { - grid-column: span 12; - } - - .org-panel-head { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/arma/ui/apps/portal/components/metricCard.css b/arma/ui/apps/portal/components/metricCard.css deleted file mode 100644 index b7d283f..0000000 --- a/arma/ui/apps/portal/components/metricCard.css +++ /dev/null @@ -1,69 +0,0 @@ -.org-metric-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -.org-metric-card { - display: flex; - flex-direction: column; - gap: 0.45rem; - padding: 1rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - - &:nth-child(4n + 2), - &:nth-child(4n + 3) { - background: linear-gradient( - 180deg, - rgb(248 250 252) 0%, - rgb(226 232 240) 100% - ); - border-color: rgb(100 116 139 / 0.35); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); - } -} - -.org-metric-label { - font-size: 0.76rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-muted); -} - -.org-metric-value { - font-size: 1.8rem; - color: var(--primary-hover); - line-height: 1.1; - - .org-metric-card:nth-child(4n + 2) &, - .org-metric-card:nth-child(4n + 3) & { - color: #334155; - } -} - -.org-metric-note { - color: var(--text-muted); - font-size: 0.9rem; -} - -@media (max-width: 960px) { - .org-metric-grid { - grid-template-columns: 1fr; - } - - .org-metric-card { - &:nth-child(4n + 3) { - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - border-color: var(--border); - box-shadow: none; - } - } - - .org-metric-value { - .org-metric-card:nth-child(4n + 3) & { - color: var(--primary-hover); - } - } -} diff --git a/arma/ui/apps/portal/components/metricCard.js b/arma/ui/apps/portal/components/metricCard.js deleted file mode 100644 index 065fe94..0000000 --- a/arma/ui/apps/portal/components/metricCard.js +++ /dev/null @@ -1,20 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.MetricCard = function MetricCard( - label, - value, - note, - ) { - return h( - "div", - { className: "org-metric-card" }, - h("span", { className: "org-metric-label" }, label), - h("strong", { className: "org-metric-value" }, value), - h("span", { className: "org-metric-note" }, note), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/modalLayer.css b/arma/ui/apps/portal/components/modalLayer.css deleted file mode 100644 index 1b2a41d..0000000 --- a/arma/ui/apps/portal/components/modalLayer.css +++ /dev/null @@ -1,131 +0,0 @@ -.org-modal-backdrop { - position: fixed; - inset: 0; - background: rgb(15 23 42 / 0.38); - display: flex; - align-items: center; - justify-content: center; - padding: 1.5rem; - z-index: 20; -} - -.org-modal-card { - width: min(100%, 30rem); - margin-bottom: 0; - text-align: left; -} - -.org-modal-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1rem; -} - -.org-modal-close { - width: 2.25rem; - height: 2.25rem; - padding: 0; - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - box-shadow: none; - transform: none; - - &:hover { - background: var(--bg-surface-hover); - color: var(--text-main); - box-shadow: none; - transform: none; - } -} - -.org-modal-form { - display: flex; - flex-direction: column; - gap: 1rem; - - label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; - } - - input, - select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: - border-color 0.2s, - box-shadow 0.2s; - - &:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); - } - - &:disabled { - background: #f1f5f9; - color: var(--text-muted); - cursor: not-allowed; - } - } -} - -.org-modal-actions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; - - button + button { - margin-left: 0; - } -} - -.org-danger-confirm { - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid #fecaca; - border-radius: var(--radius); - background: #fff1f2; - align-items: flex-start; - - p { - margin: 0; - color: var(--text-main); - } -} - -.org-danger-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - - button + button { - margin-left: 0; - } -} - -@media (max-width: 960px) { - .org-modal-head, - .org-danger-confirm { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/arma/ui/apps/portal/components/overviewCard.css b/arma/ui/apps/portal/components/overviewCard.css deleted file mode 100644 index e961dde..0000000 --- a/arma/ui/apps/portal/components/overviewCard.css +++ /dev/null @@ -1,58 +0,0 @@ -.org-hero-grid { - display: grid; - grid-template-columns: 1.3fr 1fr; - gap: 1.5rem; - align-items: start; -} - -.org-summary { - margin: 0; - font-size: 1.05rem; - color: var(--text-main); -} - -.org-meta-row { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1rem; - margin-top: 1.5rem; -} - -.org-meta-item { - display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - - &:nth-child(even) { - background: linear-gradient( - 180deg, - rgb(241 245 249) 0%, - rgb(226 232 240) 100% - ); - border-color: rgb(148 163 184 / 0.45); - } -} - -.org-meta-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -.org-meta-value { - font-size: 1rem; - font-weight: 600; - color: var(--primary-hover); -} - -@media (max-width: 960px) { - .org-hero-grid, - .org-meta-row { - grid-template-columns: 1fr; - } -} diff --git a/arma/ui/apps/portal/components/portalHeader.js b/arma/ui/apps/portal/components/portalHeader.js deleted file mode 100644 index 9ec5cf0..0000000 --- a/arma/ui/apps/portal/components/portalHeader.js +++ /dev/null @@ -1,30 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData, session } = OrgPortal.data; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.PortalHeader = function PortalHeader() { - return h( - "section", - { className: "card org-panel org-span-12 org-page-header" }, - h( - "div", - { className: "org-page-heading" }, - h("span", { className: "org-page-kicker" }, portalData.org.tag), - h("h1", { className: "org-page-title" }, portalData.org.name), - h( - "p", - { className: "org-page-subtitle" }, - "Player organization command portal", - ), - h( - "span", - { className: "org-page-meta" }, - `${session.actorName} - ${session.role}`, - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/simpleList.css b/arma/ui/apps/portal/components/simpleList.css deleted file mode 100644 index be49ccc..0000000 --- a/arma/ui/apps/portal/components/simpleList.css +++ /dev/null @@ -1,67 +0,0 @@ -.org-simple-list, -.org-name-list, -.org-activity-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; -} - -.org-simple-list, -.org-name-list, -.org-activity-list, -.org-roadmap-grid { - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -.org-simple-row, -.org-name-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - - &:nth-child(even) { - background: linear-gradient( - 180deg, - rgb(248 250 252) 0%, - rgb(241 245 249) 100% - ); - border-color: rgb(148 163 184 / 0.45); - } -} - -.org-simple-name { - color: var(--primary-hover); -} - -.org-simple-meta { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 1rem; -} - -.org-name-row { - justify-content: flex-start; - - button { - margin-left: auto; - } -} - -@media (max-width: 960px) { - .org-simple-row, - .org-name-row { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/arma/ui/apps/portal/components/simpleStat.css b/arma/ui/apps/portal/components/simpleStat.css deleted file mode 100644 index 824ad98..0000000 --- a/arma/ui/apps/portal/components/simpleStat.css +++ /dev/null @@ -1,18 +0,0 @@ -.org-simple-stat { - display: flex; - flex-direction: column; - gap: 0.2rem; - min-width: 90px; -} - -.org-simple-label { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -.org-simple-value { - font-size: 0.95rem; - color: var(--text-main); -} diff --git a/arma/ui/apps/portal/components/simpleStat.js b/arma/ui/apps/portal/components/simpleStat.js deleted file mode 100644 index 9922ac8..0000000 --- a/arma/ui/apps/portal/components/simpleStat.js +++ /dev/null @@ -1,15 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { - return h( - "div", - { className: "org-simple-stat" }, - h("span", { className: "org-simple-label" }, label), - h("strong", { className: "org-simple-value" }, value), - ); - }; -})(); diff --git a/arma/ui/apps/portal/components/treasuryCard.css b/arma/ui/apps/portal/components/treasuryCard.css deleted file mode 100644 index 26a4417..0000000 --- a/arma/ui/apps/portal/components/treasuryCard.css +++ /dev/null @@ -1,99 +0,0 @@ -.org-finance-meta { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - margin-bottom: 1.5rem; - - > div { - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - display: flex; - flex-direction: column; - gap: 0.4rem; - } -} - -.org-action-grid { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; - - button + button { - margin-left: 0; - } - - button { - width: 100%; - } -} - -.org-treasury-notice { - margin-bottom: 1rem; - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; - - &.is-success { - background: #ecfdf5; - border: 1px solid #bbf7d0; - color: #166534; - } - - &.is-error { - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; - } -} - -.org-credit-lines { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.org-access-note { - margin: 0 0 1rem; - color: var(--text-muted); - font-size: 0.95rem; -} - -.org-credit-list { - display: flex; - flex-direction: column; - gap: 0.6rem; -} - -.org-credit-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.9rem 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - - &:nth-child(even) { - background: linear-gradient( - 180deg, - rgb(248 250 252) 0%, - rgb(241 245 249) 100% - ); - border-color: rgb(148 163 184 / 0.45); - } -} - -@media (max-width: 960px) { - .org-finance-meta { - grid-template-columns: 1fr; - } - - .org-credit-row { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/arma/ui/apps/portal/components/treasuryCard.js b/arma/ui/apps/portal/components/treasuryCard.js deleted file mode 100644 index b665f93..0000000 --- a/arma/ui/apps/portal/components/treasuryCard.js +++ /dev/null @@ -1,126 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const permissions = OrgPortal.permissions; - const actions = OrgPortal.actions; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { - const notice = store.getTreasuryNotice(); - const creditLines = store.getCreditLines(); - const allowTreasuryActions = permissions.canManageTreasury(); - - return h( - "section", - { className: "card org-panel org-span-5" }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - h("h2", { className: "org-panel-title" }, "Treasury"), - h( - "p", - { className: "org-panel-subtitle" }, - "Organization funds, reputation, and member payouts.", - ), - ), - ), - h( - "div", - { className: "org-finance-meta" }, - h( - "div", - null, - h("span", { className: "org-meta-label" }, "Funds"), - h("strong", null, actions.formatCurrency(store.getFunds())), - ), - h( - "div", - null, - h("span", { className: "org-meta-label" }, "Reputation"), - h("strong", null, `${portalData.reputation}`), - ), - ), - allowTreasuryActions - ? h( - "div", - { className: "org-action-grid" }, - h( - "button", - { - type: "button", - onClick: () => actions.openModal("payroll"), - }, - "Run Payroll", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.openModal("transfer"), - }, - "Send Funds", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.openModal("credit"), - }, - "Credit Line", - ), - ) - : h( - "p", - { className: "org-access-note" }, - "Only the organization leader or CEO can manage treasury actions.", - ), - notice.text - ? h( - "div", - { - className: - notice.type === "error" - ? "org-treasury-notice is-error" - : "org-treasury-notice is-success", - }, - notice.text, - ) - : null, - creditLines.length > 0 - ? h( - "div", - { className: "org-credit-lines" }, - h( - "span", - { className: "org-meta-label" }, - "Active Credit Lines", - ), - h( - "div", - { className: "org-credit-list" }, - ...creditLines.map((line) => - h( - "div", - { className: "org-credit-row" }, - h("span", null, line.member), - h( - "strong", - null, - actions.formatCurrency(line.amount), - ), - ), - ), - ), - ) - : null, - ); - }; -})(); diff --git a/arma/ui/apps/portal/permissions.js b/arma/ui/apps/portal/permissions.js index ee67d4f..b25ec78 100644 --- a/arma/ui/apps/portal/permissions.js +++ b/arma/ui/apps/portal/permissions.js @@ -1,71 +1,10 @@ (function () { const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); const { portalData, session } = OrgPortal.data; + const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - class OrgPortalPermissions { - getNormalizedRole() { - return String(session.role || "") - .trim() - .toUpperCase(); - } - - isDefaultOrg() { - return ( - portalData.org.isDefault === true || - String(portalData.org.tag || "") - .trim() - .toUpperCase() === "DEFAULT" - ); - } - - isOrgOwner() { - const ownerUid = String( - portalData.org.ownerUid || portalData.org.owner || "", - ) - .trim() - .toLowerCase(); - const actorUid = String(session.actorUid || "") - .trim() - .toLowerCase(); - - if (ownerUid && actorUid) { - return actorUid === ownerUid; - } - - return ( - String(session.actorName || "") - .trim() - .toLowerCase() === - String(portalData.org.owner || "") - .trim() - .toLowerCase() - ); - } - - isSessionCeo() { - return session.ceo === true; - } - - isOrgLeaderOrCeo() { - return ( - this.isOrgOwner() || - this.getNormalizedRole() === "LEADER" || - (this.isDefaultOrg() && this.isSessionCeo()) - ); - } - - canManageMembers() { - return this.isOrgLeaderOrCeo(); - } - - canManageTreasury() { - return this.isOrgLeaderOrCeo(); - } - - canDisbandOrg() { - return this.isOrgLeaderOrCeo(); - } - } - - OrgPortal.permissions = new OrgPortalPermissions(); + OrgPortal.permissions = SharedLogic.createPortalPermissions({ + portalData, + session, + }); })(); diff --git a/arma/ui/apps/portal/runtime.js b/arma/ui/apps/portal/runtime.js deleted file mode 100644 index 8798b3b..0000000 --- a/arma/ui/apps/portal/runtime.js +++ /dev/null @@ -1,98 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - - const SVG_NS = "http://www.w3.org/2000/svg"; - const SVG_TAGS = new Set([ - "svg", - "path", - "circle", - "rect", - "line", - "polyline", - "polygon", - "g", - "defs", - "use", - "text", - "tspan", - "clipPath", - "mask", - ]); - - function h(tag, props = {}, ...children) { - const isSvg = SVG_TAGS.has(tag); - const el = isSvg - ? document.createElementNS(SVG_NS, tag) - : document.createElement(tag); - - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith("on") && typeof value === "function") { - el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === "className") { - if (isSvg) { - el.setAttribute("class", value); - } else { - el.className = value; - } - } else if (key === "style" && typeof value === "object") { - Object.assign(el.style, value); - } else if (typeof value === "boolean") { - if (value) { - el.setAttribute(key, ""); - } else { - el.removeAttribute(key); - } - } else if (value === null || value === undefined) { - el.removeAttribute(key); - } else { - el.setAttribute(key, value); - } - }); - } - - 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) => el.appendChild(c)); - } - }); - - return el; - } - - let rootContainer = null; - let rootComponent = null; - - function render(component, container) { - rootContainer = container; - rootComponent = component; - rerender(); - } - - function rerender() { - rootContainer.innerHTML = ""; - rootContainer.appendChild(rootComponent()); - } - - function createSignal(initialValue) { - let value = initialValue; - - const getValue = () => value; - const setValue = (newValue) => { - value = typeof newValue === "function" ? newValue(value) : newValue; - rerender(); - }; - - return [getValue, setValue]; - } - - OrgPortal.runtime = { - h, - render, - createSignal, - }; -})(); diff --git a/arma/ui/apps/portal/store.js b/arma/ui/apps/portal/store.js index e75e120..5f2711e 100644 --- a/arma/ui/apps/portal/store.js +++ b/arma/ui/apps/portal/store.js @@ -2,31 +2,10 @@ const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); const { createSignal } = window.RegistryApp.runtime; const { portalData } = OrgPortal.data; + const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - class OrgPortalStore { - constructor() { - [this.getFunds, this.setFunds] = createSignal(portalData.funds); - [this.getMembers, this.setMembers] = createSignal([ - ...portalData.members, - ]); - [this.getCreditLines, this.setCreditLines] = createSignal([]); - [this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({ - type: "", - text: "", - }); - [this.getModal, this.setModal] = createSignal(null); - [this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false); - } - - hydrateFromPayload(payload) { - this.setFunds(payload.portalData.funds || 0); - this.setMembers([...(payload.portalData.members || [])]); - this.setCreditLines([]); - this.setTreasuryNotice({ type: "", text: "" }); - this.setModal(null); - this.setOrgDisbanded(false); - } - } - - OrgPortal.store = new OrgPortalStore(); + OrgPortal.store = SharedLogic.createPortalStore({ + createSignal, + portalData, + }); })(); diff --git a/arma/ui/apps/main/runtime.js b/arma/ui/apps/runtime.js similarity index 79% rename from arma/ui/apps/main/runtime.js rename to arma/ui/apps/runtime.js index 522a146..fce44e9 100644 --- a/arma/ui/apps/main/runtime.js +++ b/arma/ui/apps/runtime.js @@ -1,5 +1,6 @@ (function () { const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_TAGS = new Set([ @@ -66,6 +67,7 @@ let rootContainer = null; let rootComponent = null; + const injectedStyles = new Set(); function render(component, container) { rootContainer = container; @@ -74,10 +76,26 @@ } function rerender() { + if (!rootContainer || !rootComponent) { + return; + } + rootContainer.innerHTML = ""; rootContainer.appendChild(rootComponent()); } + function ensureScopedStyle(id, cssText) { + if (!id || !cssText || injectedStyles.has(id)) { + return; + } + + const style = document.createElement("style"); + style.setAttribute("data-ui-style", id); + style.textContent = cssText; + document.head.appendChild(style); + injectedStyles.add(id); + } + function createSignal(initialValue) { let value = initialValue; @@ -90,9 +108,15 @@ return [getValue, setValue]; } - RegistryApp.runtime = { + const runtime = { h, render, createSignal, + ensureScopedStyle, + rerender, }; + + RegistryApp.runtime = runtime; + OrgPortal.runtime = runtime; + window.AppRuntime = runtime; })(); diff --git a/arma/ui/apps/views/DisbandedView.js b/arma/ui/apps/views/DisbandedView.js new file mode 100644 index 0000000..01659e0 --- /dev/null +++ b/arma/ui/apps/views/DisbandedView.js @@ -0,0 +1,36 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h } = OrgPortal.runtime; + const { portalData } = OrgPortal.data; + const registryStore = window.RegistryApp.store; + + OrgPortal.componentFns = OrgPortal.componentFns || {}; + + OrgPortal.componentFns.DisbandedView = function DisbandedView() { + const PanelCard = window.SharedUI.componentFns.PanelCard; + + return PanelCard({ + className: "org-span-12 org-empty-state", + eyebrow: "Organization Removed", + title: portalData.org.name, + body: h( + "div", + null, + h( + "p", + { className: "org-summary" }, + "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", + ), + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => registryStore.setView("home"), + }, + "Return to Registry", + ), + ), + }); + }; +})(); diff --git a/arma/ui/apps/views/HomeView.js b/arma/ui/apps/views/HomeView.js new file mode 100644 index 0000000..be9a07b --- /dev/null +++ b/arma/ui/apps/views/HomeView.js @@ -0,0 +1,97 @@ +(function () { + const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); + const { h, ensureScopedStyle } = RegistryApp.runtime; + const store = RegistryApp.store; + const bridge = RegistryApp.bridge; + const scopeAttr = "data-ui-home-view"; + const scopeSelector = `[${scopeAttr}]`; + const homeViewCss = ` +${scopeSelector} { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} + +${scopeSelector} .home-span-full { + grid-column: span 2; +} + +${scopeSelector} .home-feedback { + padding: 0.85rem 1rem; + border-radius: var(--radius); + font-size: 0.92rem; + background: #fef2f2; + border: 1px solid #fecaca; + color: #991b1b; +} + +@media (max-width: 960px) { + ${scopeSelector} { + grid-template-columns: 1fr; + } + + ${scopeSelector} .home-span-full { + grid-column: span 1; + } +} +`; + + RegistryApp.componentFns = RegistryApp.componentFns || {}; + + RegistryApp.componentFns.HomeView = function HomeView() { + const isAuthenticating = store.getIsAuthenticating(); + const loginError = store.getLoginError(); + ensureScopedStyle("main-home-view", homeViewCss); + + return h( + "div", + { className: "content", [scopeAttr]: "" }, + h( + "div", + { className: "card" }, + h("h2", null, "Create Organization"), + h( + "p", + null, + "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", + ), + h( + "button", + { onClick: () => store.setView("create") }, + "Register", + ), + ), + h( + "div", + { className: "card" }, + h("h2", null, "Organization Portal"), + h( + "p", + null, + "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", + ), + loginError + ? h("div", { className: "home-feedback" }, loginError) + : null, + h( + "button", + { + disabled: isAuthenticating, + onClick: () => { + if (!bridge) { + store.failLogin( + "Login bridge is not available.", + ); + return; + } + + bridge.requestLogin({}); + }, + }, + isAuthenticating ? "Opening Portal..." : "Login", + ), + ), + ); + }; +})(); diff --git a/arma/ui/apps/views/PortalView.js b/arma/ui/apps/views/PortalView.js new file mode 100644 index 0000000..8bf61b8 --- /dev/null +++ b/arma/ui/apps/views/PortalView.js @@ -0,0 +1,206 @@ +(function () { + const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); + const { h, ensureScopedStyle } = OrgPortal.runtime; + const { portalData, session } = OrgPortal.data; + const store = OrgPortal.store; + const portalViewScope = "[data-ui-portal-view]"; + + ensureScopedStyle( + "portal-view", + ` + ${portalViewScope} .org-toast-stack { + position: fixed; + top: 1.5rem; + right: 2rem; + z-index: 20; + display: flex; + flex-direction: column; + gap: 0.75rem; + pointer-events: none; + } + + ${portalViewScope} .org-toast { + max-width: 24rem; + padding: 0.9rem 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: #fff; + box-shadow: 0 12px 28px rgb(15 23 42 / 0.14); + font-size: 0.92rem; + pointer-events: auto; + } + + ${portalViewScope} .org-toast.is-success { + background: #ecfdf5; + border-color: #bbf7d0; + color: #166534; + } + + ${portalViewScope} .org-toast.is-error { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; + } + + ${portalViewScope} .org-dashboard-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 1.5rem; + } + + ${portalViewScope} .org-panel { + margin-bottom: 0; + text-align: left; + } + + ${portalViewScope} .org-scroll-panel { + display: flex; + flex-direction: column; + max-height: 31rem; + overflow: hidden; + } + + ${portalViewScope} .org-span-12 { + grid-column: span 12; + } + + ${portalViewScope} .org-span-7 { + grid-column: span 7; + } + + ${portalViewScope} .org-span-6 { + grid-column: span 6; + } + + ${portalViewScope} .org-span-5 { + grid-column: span 5; + } + + @media (max-width: 960px) { + ${portalViewScope} .org-toast-stack { + top: 1rem; + right: 1rem; + left: 1rem; + } + + ${portalViewScope} .org-toast { + max-width: none; + } + + ${portalViewScope} .org-span-12, + ${portalViewScope} .org-span-7, + ${portalViewScope} .org-span-6, + ${portalViewScope} .org-span-5 { + grid-column: span 12; + } + } + `, + ); + + OrgPortal.components = OrgPortal.components || {}; + + OrgPortal.components.App = function App() { + const Hero = window.SharedUI.componentFns.Hero; + const Footer = window.SharedUI.componentFns.Footer; + const OverviewCard = OrgPortal.componentFns.OverviewCard; + const FleetCard = OrgPortal.componentFns.FleetCard; + const TreasuryCard = OrgPortal.componentFns.TreasuryCard; + const MembersCard = OrgPortal.componentFns.MembersCard; + const AssetsCard = OrgPortal.componentFns.AssetsCard; + const ActivityCard = OrgPortal.componentFns.ActivityCard; + const FutureCard = OrgPortal.componentFns.FutureCard; + const DangerCard = OrgPortal.componentFns.DangerCard; + const ModalLayer = OrgPortal.componentFns.ModalLayer; + const DisbandedView = OrgPortal.componentFns.DisbandedView; + const treasuryNotice = store.getTreasuryNotice(); + const footerSections = [ + { + title: "Organization Controls", + items: [ + "Roster Management", + "Fleet Assignment", + "Treasury Permissions", + "Asset Registry", + ], + }, + { + title: "Planned Extensions", + items: [ + "Contracts Board", + "Diplomacy Layer", + "Procurement Queue", + "Reputation History", + ], + }, + ]; + + if (store.getOrgDisbanded()) { + return h( + "main", + { "data-ui-portal-view": "" }, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + Hero({ + kicker: portalData.org.tag, + title: portalData.org.name, + subtitle: "Player organization command portal", + meta: `${session.actorName} - ${session.role}`, + }), + DisbandedView(), + ), + ), + ModalLayer(), + Footer({ sections: footerSections }), + ); + } + + return h( + "main", + { "data-ui-portal-view": "" }, + treasuryNotice.text + ? h( + "div", + { className: "org-toast-stack" }, + h( + "div", + { + className: + treasuryNotice.type === "error" + ? "org-toast is-error" + : "org-toast is-success", + }, + treasuryNotice.text, + ), + ) + : null, + h( + "div", + { className: "container" }, + h( + "div", + { className: "org-dashboard-grid" }, + Hero({ + kicker: portalData.org.tag, + title: portalData.org.name, + subtitle: "Player organization command portal", + meta: `${session.actorName} - ${session.role}`, + }), + OverviewCard(), + FleetCard(), + TreasuryCard(), + MembersCard(), + AssetsCard(), + ActivityCard(), + FutureCard(), + DangerCard(), + ), + ), + ModalLayer(), + Footer({ sections: footerSections }), + ); + }; +})(); diff --git a/arma/ui/apps/main/components/createOrgForm.js b/arma/ui/apps/views/RegistrationView.js similarity index 56% rename from arma/ui/apps/main/components/createOrgForm.js rename to arma/ui/apps/views/RegistrationView.js index 7ca2a86..841abf0 100644 --- a/arma/ui/apps/main/components/createOrgForm.js +++ b/arma/ui/apps/views/RegistrationView.js @@ -1,26 +1,175 @@ (function () { const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; + const { h, ensureScopedStyle } = RegistryApp.runtime; const store = RegistryApp.store; + const bridge = RegistryApp.bridge; + const scopeAttr = "data-ui-registration-view"; + const scopeSelector = `[${scopeAttr}]`; + const registrationViewCss = ` +${scopeSelector} { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: center; + width: 100%; +} + +${scopeSelector} .info-panel { + text-align: left; + padding: 1rem; +} + +${scopeSelector} .create-feature-list { + text-align: left; + margin-top: 1.5rem; + list-style-type: none; + padding: 0; +} + +${scopeSelector} .create-feature-item { + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +${scopeSelector} .create-feature-icon { + width: 1.2rem; + height: 1.2rem; + flex-shrink: 0; +} + +${scopeSelector} .price-tag { + margin-top: 2rem; + padding: 1rem; + background: var(--bg-app); + border-radius: var(--radius); + border: 1px solid var(--border); +} + +${scopeSelector} .price-label { + display: block; + font-size: 0.9rem; + color: var(--text-muted); +} + +${scopeSelector} .price-value { + display: block; + font-size: 2rem; + font-weight: 700; + color: var(--primary); +} + +${scopeSelector} .form-panel { + margin: 0; +} + +${scopeSelector} .app-form { + display: flex; + flex-direction: column; + gap: 1rem; + text-align: left; +} + +${scopeSelector} .app-form label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-weight: 500; + font-size: 0.9rem; +} + +${scopeSelector} .app-form input, +${scopeSelector} .app-form select { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-app); + color: var(--text-main); + font-family: inherit; + font-size: 1rem; + box-sizing: border-box; + transition: border-color 0.2s; +} + +${scopeSelector} .app-form input:focus, +${scopeSelector} .app-form select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); +} + +${scopeSelector} .form-actions { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +${scopeSelector} .submit-btn { + width: 100%; +} + +${scopeSelector} .cancel-link { + font-size: 0.9rem; + color: var(--text-muted); + cursor: pointer; + text-decoration: underline; +} + +${scopeSelector} .cancel-link:hover { + color: var(--primary); +} + +${scopeSelector} .form-feedback { + padding: 0.85rem 1rem; + border-radius: var(--radius); + font-size: 0.92rem; +} + +${scopeSelector} .form-feedback.is-error { + background: #fef2f2; + border: 1px solid #fecaca; + color: #991b1b; +} + +@media (max-width: 960px) { + ${scopeSelector} { + grid-template-columns: 1fr; + } +} +`; RegistryApp.componentFns = RegistryApp.componentFns || {}; - RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() { + RegistryApp.componentFns.RegistrationView = function RegistrationView() { + const isCreating = store.getIsCreating(); + const createError = store.getCreateError(); + ensureScopedStyle("main-registration-view", registrationViewCss); + const handleCreate = () => { const data = { orgName: String( document.getElementById("org-create-name")?.value || "", - ), + ).trim(), type: String( document.getElementById("org-create-type")?.value || "", ), }; - console.log("Org Registration:", data); + + if (!bridge || typeof bridge.requestCreateOrg !== "function") { + store.failCreate("Registration bridge is not available."); + return; + } + + bridge.requestCreateOrg(data); }; return h( "div", - { className: "split-container" }, + { className: "split-container", [scopeAttr]: "" }, h( "div", { className: "info-panel" }, @@ -32,24 +181,10 @@ ), h( "ul", - { - style: { - textAlign: "left", - marginTop: "1.5rem", - listStyleType: "none", - padding: 0, - }, - }, + { className: "create-feature-list" }, h( "li", - { - style: { - marginBottom: "0.5rem", - display: "flex", - alignItems: "center", - gap: "0.5rem", - }, - }, + { className: "create-feature-item" }, h( "svg", { @@ -59,11 +194,7 @@ "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", - style: { - width: "1.2rem", - height: "1.2rem", - flexShrink: "0", - }, + className: "create-feature-icon", }, h("path", { d: "M20 6L9 17l-5-5" }), ), @@ -71,14 +202,7 @@ ), h( "li", - { - style: { - marginBottom: "0.5rem", - display: "flex", - alignItems: "center", - gap: "0.5rem", - }, - }, + { className: "create-feature-item" }, h( "svg", { @@ -88,11 +212,7 @@ "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", - style: { - width: "1.2rem", - height: "1.2rem", - flexShrink: "0", - }, + className: "create-feature-icon", }, h("path", { d: "M20 6L9 17l-5-5" }), ), @@ -100,14 +220,7 @@ ), h( "li", - { - style: { - marginBottom: "0.5rem", - display: "flex", - alignItems: "center", - gap: "0.5rem", - }, - }, + { className: "create-feature-item" }, h( "svg", { @@ -117,11 +230,7 @@ "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", - style: { - width: "1.2rem", - height: "1.2rem", - flexShrink: "0", - }, + className: "create-feature-icon", }, h("path", { d: "M20 6L9 17l-5-5" }), ), @@ -129,14 +238,7 @@ ), h( "li", - { - style: { - marginBottom: "0.5rem", - display: "flex", - alignItems: "center", - gap: "0.5rem", - }, - }, + { className: "create-feature-item" }, h( "svg", { @@ -146,11 +248,7 @@ "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", - style: { - width: "1.2rem", - height: "1.2rem", - flexShrink: "0", - }, + className: "create-feature-icon", }, h("path", { d: "M20 6L9 17l-5-5" }), ), @@ -159,44 +257,14 @@ ), h( "div", - { - className: "price-tag", - style: { - marginTop: "2rem", - padding: "1rem", - background: "var(--bg-app)", - borderRadius: "var(--radius)", - border: "1px solid var(--border)", - }, - }, - h( - "span", - { - style: { - display: "block", - fontSize: "0.9rem", - color: "var(--text-muted)", - }, - }, - "Registration Fee", - ), - h( - "span", - { - style: { - display: "block", - fontSize: "2rem", - fontWeight: "700", - color: "var(--primary)", - }, - }, - "$50,000", - ), + { className: "price-tag" }, + h("span", { className: "price-label" }, "Registration Fee"), + h("span", { className: "price-value" }, "$50,000"), ), ), h( "div", - { className: "form-panel card", style: { margin: 0 } }, + { className: "form-panel card" }, h("h2", null, "Organization Registration"), h( "div", @@ -239,14 +307,24 @@ h( "div", { className: "form-actions" }, + createError + ? h( + "div", + { className: "form-feedback is-error" }, + createError, + ) + : null, h( "button", { type: "button", - style: { width: "100%" }, + className: "submit-btn", + disabled: isCreating, onClick: handleCreate, }, - "Submit Registration", + isCreating + ? "Submitting Registration..." + : "Submit Registration", ), h( "span",