@spacemade/ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (594) hide show
  1. package/README.md +705 -0
  2. package/app.sh +806 -0
  3. package/cmd.sh +152 -0
  4. package/docs/cms-quickstart.md +206 -0
  5. package/docs/gallery-quickstart.md +24 -0
  6. package/docs/gpu-marketplace-quickstart.md +27 -0
  7. package/docs/metaverse-quickstart.md +25 -0
  8. package/docs/migration-plan.md +41 -0
  9. package/docs/modules.md +34 -0
  10. package/docs/nft-quickstart.md +26 -0
  11. package/docs/simple-cms-quickstart.md +22 -0
  12. package/middleware.ts +77 -0
  13. package/next-env.d.ts +6 -0
  14. package/next.config.ts +39 -0
  15. package/package.json +92 -0
  16. package/public/android-chrome-192x192.png +0 -0
  17. package/public/android-chrome-512x512.png +0 -0
  18. package/public/apple-touch-icon.png +0 -0
  19. package/public/favicon-16x16.png +0 -0
  20. package/public/favicon-32x32.png +0 -0
  21. package/public/favicon.ico +0 -0
  22. package/public/images/default-profile/1.png +0 -0
  23. package/public/images/default-profile/2.png +0 -0
  24. package/public/images/default-profile/3.png +0 -0
  25. package/public/images/default-profile/4.png +0 -0
  26. package/public/images/default-profile/5.png +0 -0
  27. package/public/images/default-profile/6.png +0 -0
  28. package/public/images/default-profile/7.png +0 -0
  29. package/public/images/defaults/Login-Person-Icon-dark.svg +13 -0
  30. package/public/images/defaults/Login-Person-Icon.svg +16 -0
  31. package/public/images/defaults/default-avatar.png +0 -0
  32. package/public/images/defaults/image-not-available.jpg +0 -0
  33. package/public/images/icons/amazon-icon.jpg +0 -0
  34. package/public/images/icons/azure-icon.png +0 -0
  35. package/public/images/icons/google-icon.jpg +0 -0
  36. package/public/site.webmanifest +1 -0
  37. package/scripts/crud-scaffold/bin/crud-scaffold.mjs +23 -0
  38. package/scripts/crud-scaffold/src/api/index.mjs +666 -0
  39. package/scripts/crud-scaffold/src/app/patches.mjs +355 -0
  40. package/scripts/crud-scaffold/src/core/fields.mjs +165 -0
  41. package/scripts/crud-scaffold/src/core/utils.mjs +161 -0
  42. package/scripts/crud-scaffold/src/frontend/index.mjs +1078 -0
  43. package/scripts/crud-scaffold/src/generator.mjs +103 -0
  44. package/scripts/crud-scaffold/src/templates/.gitkeep +1 -0
  45. package/scripts/crud-scaffold/src/utils/generator-utils.mjs +35 -0
  46. package/scripts/defs/cms-definition.mjs +1393 -0
  47. package/scripts/defs/gallery-definition.mjs +171 -0
  48. package/scripts/defs/gpu-definition.mjs +174 -0
  49. package/scripts/defs/metaverse-definition.mjs +170 -0
  50. package/scripts/defs/nft-definition.mjs +191 -0
  51. package/scripts/defs/simple-cms-definition.mjs +141 -0
  52. package/scripts/generate-cms-install-system.mjs +4 -0
  53. package/scripts/generate-gallery-system.mjs +4 -0
  54. package/scripts/generate-gpu-marketplace-system.mjs +4 -0
  55. package/scripts/generate-metaverse-system.mjs +4 -0
  56. package/scripts/generate-nft-system.mjs +4 -0
  57. package/scripts/generate-simple-cms-system.mjs +4 -0
  58. package/scripts/link-core.mjs +72 -0
  59. package/scripts/spacemade.mjs +446 -0
  60. package/scripts/test-generate-app.mjs +60 -0
  61. package/src/components/Dashboard/DashboardList.tsx +70 -0
  62. package/src/components/Dashboard/dashboardLayout.tsx +57 -0
  63. package/src/components/admin/nav/AdminLeftNav.tsx +168 -0
  64. package/src/components/auth/AuthBootstrap.tsx +47 -0
  65. package/src/components/auth/auth-card-shell.tsx +23 -0
  66. package/src/components/auth/forgot-password-form-template.tsx +37 -0
  67. package/src/components/auth/form/ForgotForm.tsx +146 -0
  68. package/src/components/auth/form/ResetPasswordForm.tsx +122 -0
  69. package/src/components/auth/form/SignInForm.tsx +139 -0
  70. package/src/components/auth/form/SignUpForm.tsx +166 -0
  71. package/src/components/auth/form/UpdateForm.tsx +165 -0
  72. package/src/components/auth/login-form-template.tsx +45 -0
  73. package/src/components/billing/CardBrandIcon.tsx +126 -0
  74. package/src/components/billing/block/BillingDetailsCard.tsx +120 -0
  75. package/src/components/billing/block/BillingHistory.tsx +143 -0
  76. package/src/components/billing/block/PlanDetails.tsx +138 -0
  77. package/src/components/billing/modal/UpdateCardDetailsModal.tsx +451 -0
  78. package/src/components/billing/modal/UpdatePlanModal.tsx +194 -0
  79. package/src/components/category/modal/UpdateCategoryModal.tsx +170 -0
  80. package/src/components/category/table/CategoryTable.tsx +137 -0
  81. package/src/components/common/Icons/BrainCircuitIcon.tsx +14 -0
  82. package/src/components/common/Icons/MagnifyingGlassChartIcon.tsx +14 -0
  83. package/src/components/common/Icons/PeopleGroupIcon.tsx +14 -0
  84. package/src/components/common/Icons/ScrewdriverWrenchIcon.tsx +14 -0
  85. package/src/components/common/Icons/SendMessageIcon.tsx +15 -0
  86. package/src/components/common/Icons/ShareIcon.tsx +22 -0
  87. package/src/components/common/Icons/XTwitterIcon.tsx +14 -0
  88. package/src/components/common/Image/OptimizedImage.tsx +17 -0
  89. package/src/components/common/SettingsPanel.tsx +119 -0
  90. package/src/components/common/badge/Badge.tsx +69 -0
  91. package/src/components/common/blocks/BackgroundImageBlock.tsx +43 -0
  92. package/src/components/common/blocks/Carousel.tsx +59 -0
  93. package/src/components/common/blocks/CodeSnippet.tsx +46 -0
  94. package/src/components/common/blocks/InfinityScrollWrapper.tsx +43 -0
  95. package/src/components/common/blocks/SplitBlock.tsx +47 -0
  96. package/src/components/common/blocks/TopImageBlock.tsx +39 -0
  97. package/src/components/common/button/Button.tsx +49 -0
  98. package/src/components/common/button/ButtonWithActions.tsx +71 -0
  99. package/src/components/common/button/DropdownButton.tsx +61 -0
  100. package/src/components/common/button/ScrollDownArrow.tsx +23 -0
  101. package/src/components/common/button/SharerActionBox.tsx +248 -0
  102. package/src/components/common/button/UserButton.tsx +209 -0
  103. package/src/components/common/contact/ContactForm.tsx +214 -0
  104. package/src/components/common/cta/Cta.tsx +18 -0
  105. package/src/components/common/dialog/ConversationFullscreenModal.tsx +95 -0
  106. package/src/components/common/dialog/DeleteDialog.tsx +90 -0
  107. package/src/components/common/dialog/MobileFiltersDrawer.tsx +60 -0
  108. package/src/components/common/dialog/RenameDialog.tsx +128 -0
  109. package/src/components/common/faq/FAQSection.tsx +119 -0
  110. package/src/components/common/filters/NameSearch.tsx +87 -0
  111. package/src/components/common/footer/SmallFooter.tsx +62 -0
  112. package/src/components/common/forms/AdvanceAccordion.tsx +154 -0
  113. package/src/components/common/forms/CheckBox.tsx +29 -0
  114. package/src/components/common/forms/DateTimePicker.tsx +101 -0
  115. package/src/components/common/forms/Dropzone.tsx +278 -0
  116. package/src/components/common/forms/EmailTagInput.tsx +67 -0
  117. package/src/components/common/forms/FieldAssistantBar.tsx +32 -0
  118. package/src/components/common/forms/HTMLEditor.tsx +48 -0
  119. package/src/components/common/forms/IconPicker.tsx +129 -0
  120. package/src/components/common/forms/Input.tsx +362 -0
  121. package/src/components/common/forms/InputColor.tsx +36 -0
  122. package/src/components/common/forms/RadioInputs.tsx +62 -0
  123. package/src/components/common/forms/Select.tsx +214 -0
  124. package/src/components/common/forms/SimpleTagsInput.tsx +89 -0
  125. package/src/components/common/forms/TagInput.tsx +111 -0
  126. package/src/components/common/forms/Textarea.tsx +99 -0
  127. package/src/components/common/forms/ToggleButton.tsx +67 -0
  128. package/src/components/common/forms/UserInvitePicker.tsx +600 -0
  129. package/src/components/common/header/AppPageTitle.tsx +34 -0
  130. package/src/components/common/header/SubtitleAccordion.tsx +63 -0
  131. package/src/components/common/home/block/GetStarted.tsx +17 -0
  132. package/src/components/common/home/block/Hero.tsx +33 -0
  133. package/src/components/common/home/block/InstallShortcut.tsx +41 -0
  134. package/src/components/common/home/block/MarketplaceBlock.tsx +34 -0
  135. package/src/components/common/home/block/OriginStoryBanner.tsx +17 -0
  136. package/src/components/common/home/block/OsirusCTABlock.tsx +21 -0
  137. package/src/components/common/home/block/PromoFeatureSlider.tsx +260 -0
  138. package/src/components/common/layout/DetailHero.tsx +87 -0
  139. package/src/components/common/layout/SideNavLayout.tsx +1016 -0
  140. package/src/components/common/layout/account/AccountLayout.tsx +99 -0
  141. package/src/components/common/layout/admin/AdminLayout.tsx +17 -0
  142. package/src/components/common/layout/explore/ExploreLayout.tsx +23 -0
  143. package/src/components/common/layout/explore/ExploreLayoutContent.tsx +20 -0
  144. package/src/components/common/layout/explore/ExploreLayoutFilters.tsx +28 -0
  145. package/src/components/common/loading/LoadingScreen.tsx +86 -0
  146. package/src/components/common/loading/Spinner.tsx +12 -0
  147. package/src/components/common/state/InvalidInviteCard.tsx +32 -0
  148. package/src/components/common/table/TableFilter.tsx +205 -0
  149. package/src/components/common/table/TableHeader.tsx +33 -0
  150. package/src/components/common/table/TablePaging.tsx +190 -0
  151. package/src/components/common/table/TablePagingButton.tsx +64 -0
  152. package/src/components/common/table/TablePagingInput.tsx +79 -0
  153. package/src/components/common/useUrlSelectedModel.ts +7 -0
  154. package/src/components/docs/DocsSitePreviewPage.tsx +122 -0
  155. package/src/components/docs/block/DocsPageDetailBlock.tsx +333 -0
  156. package/src/components/docs/block/DocsSidebarTopContent.tsx +8 -0
  157. package/src/components/docs/block/DocsSitePagesBlock.tsx +81 -0
  158. package/src/components/docs/block/DocsSitesBlock.tsx +35 -0
  159. package/src/components/docs/modal/CreateDocsSiteModal.tsx +132 -0
  160. package/src/components/docs/table/DocsPagesTable.tsx +163 -0
  161. package/src/components/docs/table/DocsSitesTable.tsx +109 -0
  162. package/src/components/groups/block/GroupInitialAvatar.tsx +53 -0
  163. package/src/components/groups/grid/OrgGroupMembersGrid.tsx +253 -0
  164. package/src/components/groups/modal/AddGroupModal.tsx +138 -0
  165. package/src/components/groups/modal/OrgGroupMembersModal.tsx +171 -0
  166. package/src/components/groups/modal/OrgGroupPermissionsModal.tsx +184 -0
  167. package/src/components/groups/table/OrgGroupsTable.tsx +470 -0
  168. package/src/components/head/DefaultMetadata.tsx +135 -0
  169. package/src/components/head/PageSeo.tsx +198 -0
  170. package/src/components/hoc/WithAuth.tsx +82 -0
  171. package/src/components/hoc/WithCustomerIO.tsx +48 -0
  172. package/src/components/hoc/WithGTM.tsx +31 -0
  173. package/src/components/hoc/WithGlobalContext.tsx +30 -0
  174. package/src/components/hoc/WithReCaptcha.tsx +31 -0
  175. package/src/components/hoc/WithStripe.tsx +68 -0
  176. package/src/components/layout/SidebarSizer.tsx +63 -0
  177. package/src/components/layout/app-left-nav.tsx +31 -0
  178. package/src/components/layout/app-main-layout.tsx +39 -0
  179. package/src/components/layout/site-footer.tsx +11 -0
  180. package/src/components/layout/site-header.tsx +19 -0
  181. package/src/components/marketplace/card/ProductCard.tsx +178 -0
  182. package/src/components/media/blocks/MediaAttachmentsList.tsx +79 -0
  183. package/src/components/media/blocks/MediaDetails.tsx +139 -0
  184. package/src/components/media/blocks/MediaGrid.tsx +245 -0
  185. package/src/components/media/form/MediaMiniForm.tsx +105 -0
  186. package/src/components/media/form/UpdateMediaForm.tsx +209 -0
  187. package/src/components/media/items/CircleMediaPicker.tsx +76 -0
  188. package/src/components/media/items/MediaAttachmentLinks.tsx +98 -0
  189. package/src/components/media/items/MediaDocumentPreview.tsx +43 -0
  190. package/src/components/media/items/MediaFileIcon.tsx +30 -0
  191. package/src/components/media/items/MediaPickerBox.tsx +94 -0
  192. package/src/components/media/items/MediaPickerButton.tsx +37 -0
  193. package/src/components/media/items/MediaSourcePreview.tsx +35 -0
  194. package/src/components/media/modal/AddMediaModal.tsx +237 -0
  195. package/src/components/media/modal/MediaAttachmentsModal.tsx +113 -0
  196. package/src/components/media/modal/MediaBulkDeleteWarningModal.tsx +142 -0
  197. package/src/components/media/modal/MediaDeleteWarningModal.tsx +98 -0
  198. package/src/components/media/modal/MediaPickerModal.tsx +201 -0
  199. package/src/components/media/table/MediaTable.tsx +340 -0
  200. package/src/components/media/table/MediaTableActions.tsx +130 -0
  201. package/src/components/media/table/MediaTableFilter.tsx +163 -0
  202. package/src/components/modules/module-card-grid.tsx +19 -0
  203. package/src/components/modules/module-page.tsx +27 -0
  204. package/src/components/modules/resource-page.tsx +15 -0
  205. package/src/components/nav/AdminMobileMenu.tsx +228 -0
  206. package/src/components/nav/AppTopNav.tsx +53 -0
  207. package/src/components/nav/MobileMenu.tsx +132 -0
  208. package/src/components/nav/TopNav.tsx +137 -0
  209. package/src/components/openapi/DeleteTokenModal.tsx +72 -0
  210. package/src/components/openapi/actions/ApiTokenActionsMenu.tsx +141 -0
  211. package/src/components/openapi/actions/ApiTokenCreateModal.tsx +107 -0
  212. package/src/components/openapi/actions/ApiTokenEditModal.tsx +168 -0
  213. package/src/components/orgs/OrgCreateModal.tsx +126 -0
  214. package/src/components/orgs/OrgJumpRedirect.tsx +150 -0
  215. package/src/components/page/SiteContentPage.tsx +129 -0
  216. package/src/components/post/accordion/FilterPostCategoriesAccordion.tsx +126 -0
  217. package/src/components/post/block/FeaturedProductCard.tsx +31 -0
  218. package/src/components/post/block/FeaturedProviderCard.tsx +51 -0
  219. package/src/components/post/block/InTheNews.tsx +77 -0
  220. package/src/components/post/block/PostCardsGrid.tsx +56 -0
  221. package/src/components/post/block/PostContent.tsx +128 -0
  222. package/src/components/post/block/PostDetailPage.tsx +95 -0
  223. package/src/components/post/block/PostList.tsx +111 -0
  224. package/src/components/post/block/PostNextAndPrevButtons.tsx +78 -0
  225. package/src/components/post/block/PostPeopleAlsoReadBlock.tsx +69 -0
  226. package/src/components/post/block/PostRelatedCards.tsx +43 -0
  227. package/src/components/post/card/FeaturedPostCard.tsx +107 -0
  228. package/src/components/post/card/InTheNewsCard.tsx +162 -0
  229. package/src/components/post/card/PostCard.tsx +125 -0
  230. package/src/components/post/card/RelatedPostCard.tsx +115 -0
  231. package/src/components/post/form/UpdatePostForm.tsx +639 -0
  232. package/src/components/post/modal/CreatePostModal.tsx +115 -0
  233. package/src/components/post/modal/PreviewPostModal.tsx +82 -0
  234. package/src/components/post/nav/BlogCategoriesSideNav.tsx +55 -0
  235. package/src/components/post/table/PostsTable.tsx +284 -0
  236. package/src/components/pricing/block/AddOnRow.tsx +78 -0
  237. package/src/components/pricing/block/CompletePurchase.tsx +546 -0
  238. package/src/components/pricing/block/PlanPicker.tsx +76 -0
  239. package/src/components/pricing/card/PricingCard.tsx +291 -0
  240. package/src/components/pricing/form/AddBillingInformationForm.tsx +212 -0
  241. package/src/components/pricing/form/AddPaymentMethodForm.tsx +252 -0
  242. package/src/components/pricing/form/CouponCodeForm.tsx +118 -0
  243. package/src/components/pricing/form/StripePaymentForm.tsx +106 -0
  244. package/src/components/product/block/ProductScreenshot.tsx +22 -0
  245. package/src/components/product/block/ProductScreenshotPreview.tsx +46 -0
  246. package/src/components/product/button/MediaScreenshotPicker.tsx +72 -0
  247. package/src/components/product/form/UpdateProductForm.tsx +718 -0
  248. package/src/components/product/modal/CreateProductModal.tsx +128 -0
  249. package/src/components/product/modal/UpdateProductCouponModal.tsx +227 -0
  250. package/src/components/product/modal/UpdateProductPlanModal.tsx +291 -0
  251. package/src/components/product/modal/UpdateProductVersionModal.tsx +236 -0
  252. package/src/components/product/table/ProductCouponsTable.tsx +100 -0
  253. package/src/components/product/table/ProductPlansTable.tsx +118 -0
  254. package/src/components/product/table/ProductVersionsAdminTable.tsx +103 -0
  255. package/src/components/product/table/ProductVersionsTable.tsx +111 -0
  256. package/src/components/product/table/ProductsTable.tsx +133 -0
  257. package/src/components/projects/ProjectFilesCard.tsx +134 -0
  258. package/src/components/projects/ProjectItemOptionButton.tsx +112 -0
  259. package/src/components/projects/ProjectPickerNav.tsx +440 -0
  260. package/src/components/projects/ProjectQuickCreateModal.tsx +159 -0
  261. package/src/components/projects/ProjectSidebarList.tsx +183 -0
  262. package/src/components/projects/actions/ProjectActionsMenu.tsx +77 -0
  263. package/src/components/projects/actions/ProjectCreateModal.tsx +577 -0
  264. package/src/components/projects/actions/ProjectEditModal.tsx +345 -0
  265. package/src/components/projects/actions/ProjectFileGrid.tsx +153 -0
  266. package/src/components/projects/actions/ProjectFilesModal.tsx +259 -0
  267. package/src/components/projects/actions/ProjectFilesPicker.tsx +31 -0
  268. package/src/components/projects/actions/ProjectFilesUploadModal.tsx +181 -0
  269. package/src/components/projects/actions/ProjectInstructions.tsx +160 -0
  270. package/src/components/projects/actions/RenameProjectModal.tsx +120 -0
  271. package/src/components/projects/actions/index.tsx +98 -0
  272. package/src/components/projects/block/HeaderProjectImagePicker.tsx +52 -0
  273. package/src/components/projects/permission/ProjectPermissionInviteModal.tsx +181 -0
  274. package/src/components/projects/permission/ProjectUserPermission.tsx +170 -0
  275. package/src/components/projects/postByCategory.tsx +118 -0
  276. package/src/components/projects/projectFileHistory.tsx +48 -0
  277. package/src/components/projects/table/ProjectTable.tsx +365 -0
  278. package/src/components/provider/BuildspecForm.tsx +288 -0
  279. package/src/components/provider/CreateProviderModal.tsx +210 -0
  280. package/src/components/provider/FeaturedProviders.tsx +137 -0
  281. package/src/components/provider/ProviderConnectModal.tsx +324 -0
  282. package/src/components/provider/form/UpdateProviderForm.tsx +449 -0
  283. package/src/components/provider/table/ProvidersTable.tsx +144 -0
  284. package/src/components/providers/ProviderFeaturesSideNav.tsx +52 -0
  285. package/src/components/settings/SettingsLayout.tsx +14 -0
  286. package/src/components/settings/SettingsSidebarNav.tsx +53 -0
  287. package/src/components/settings/settingsNav.ts +96 -0
  288. package/src/components/site/modal/UpdateSiteModal.tsx +134 -0
  289. package/src/components/site/table/SiteTable.tsx +198 -0
  290. package/src/components/starter/list-panel.tsx +12 -0
  291. package/src/components/starter/starter-overview.tsx +32 -0
  292. package/src/components/starter/stats-grid.tsx +20 -0
  293. package/src/components/storage/VectorStoreFileDeleteButton.tsx +71 -0
  294. package/src/components/storage/actions/VectorStoreActionsMenu.tsx +70 -0
  295. package/src/components/storage/actions/VectorStoreEditModal.tsx +220 -0
  296. package/src/components/storage/actions/VectorStoreFileActions.tsx +61 -0
  297. package/src/components/storage/actions/index.tsx +76 -0
  298. package/src/components/storage/addStorageModal.tsx +552 -0
  299. package/src/components/storage/table/vectorStoreTable.tsx +461 -0
  300. package/src/components/storage/vectorFilesModal.tsx +209 -0
  301. package/src/components/storage/vectorStoreFileGrid.tsx +384 -0
  302. package/src/components/tag/modal/UpdateTagModal.tsx +192 -0
  303. package/src/components/tag/table/TagTable.tsx +138 -0
  304. package/src/components/user/block/BadgesBlock.tsx +26 -0
  305. package/src/components/user/form/ChangePasswordForm.tsx +119 -0
  306. package/src/components/user/form/ProfileForm.tsx +168 -0
  307. package/src/components/user/grid/UserGrid.tsx +475 -0
  308. package/src/components/user/modal/AddUserModal.tsx +165 -0
  309. package/src/components/user/modal/UpdateUserDetailsModal.tsx +319 -0
  310. package/src/components/user/modal/UpgradeUserLimitModal.tsx +123 -0
  311. package/src/components/user/modal/UserPermissionsModal.tsx +251 -0
  312. package/src/components/user/table/UserTable.tsx +202 -0
  313. package/src/config/app.ts +16 -0
  314. package/src/config/modules.ts +100 -0
  315. package/src/config/navigation.ts +28 -0
  316. package/src/config/runtime.ts +3 -0
  317. package/src/context/CheckoutContext.tsx +123 -0
  318. package/src/context/ProjectContext.tsx +123 -0
  319. package/src/context/RouterContext.tsx +127 -0
  320. package/src/context/UIContext.tsx +47 -0
  321. package/src/context/UserAuthContext.tsx +26 -0
  322. package/src/context/VectorStoreContext.tsx +51 -0
  323. package/src/context/index.ts +2 -0
  324. package/src/data/bootstrap-icons.json +2080 -0
  325. package/src/data/bootstrap.ts +196 -0
  326. package/src/data/bootstrapIcons.ts +28 -0
  327. package/src/enum/AppEnvironments.ts +4 -0
  328. package/src/enum/HTTPErrorStatus.ts +10 -0
  329. package/src/helpers/api/index.ts +123 -0
  330. package/src/helpers/constants/index.ts +4 -0
  331. package/src/helpers/constants/messages.ts +1 -0
  332. package/src/helpers/constants/regex.ts +10 -0
  333. package/src/helpers/constants/stripe.ts +1 -0
  334. package/src/helpers/constants/system-agent.ts +12 -0
  335. package/src/helpers/data-transform/bytesToSize.ts +8 -0
  336. package/src/helpers/data-transform/dropzone.ts +116 -0
  337. package/src/helpers/data-transform/filterUtils.ts +21 -0
  338. package/src/helpers/data-transform/index.ts +4 -0
  339. package/src/helpers/data-transform/operations.ts +3 -0
  340. package/src/helpers/data-transform/paging.ts +24 -0
  341. package/src/helpers/data-transform/stringCaseTransform.ts +34 -0
  342. package/src/helpers/date/index.ts +149 -0
  343. package/src/helpers/error/formErrorHandler.tsx +38 -0
  344. package/src/helpers/images/assets.ts +32 -0
  345. package/src/helpers/images/defaultProfileImage.ts +21 -0
  346. package/src/helpers/images/fileType.ts +123 -0
  347. package/src/helpers/images/index.ts +3 -0
  348. package/src/helpers/navigation/routeScroll.ts +29 -0
  349. package/src/helpers/pageScreenshot.ts +176 -0
  350. package/src/helpers/permissions/index.ts +292 -0
  351. package/src/helpers/providers/providerFeatures.ts +29 -0
  352. package/src/helpers/sharer/index.ts +44 -0
  353. package/src/helpers/sitemap/sitemap-utils.ts +50 -0
  354. package/src/helpers/slugUtils/index.ts +39 -0
  355. package/src/helpers/sonicVoices.ts +22 -0
  356. package/src/helpers/string/index.ts +93 -0
  357. package/src/helpers/theme/colors.ts +27 -0
  358. package/src/helpers/types/api.ts +22 -0
  359. package/src/helpers/types/customer-io.ts +47 -0
  360. package/src/helpers/types/data-layer.ts +25 -0
  361. package/src/helpers/types/index.ts +4 -0
  362. package/src/helpers/types/nav.ts +13 -0
  363. package/src/helpers/types/page-props.ts +31 -0
  364. package/src/helpers/types/select-option.ts +4 -0
  365. package/src/helpers/types/utils.ts +7 -0
  366. package/src/helpers/url/index.ts +26 -0
  367. package/src/hooks/auth/useAccountStore.ts +69 -0
  368. package/src/hooks/auth/useAuthStore.ts +162 -0
  369. package/src/hooks/category/useCategoryStore.ts +71 -0
  370. package/src/hooks/index.ts +17 -0
  371. package/src/hooks/media/useMediaStore.ts +46 -0
  372. package/src/hooks/post/usePostStore.ts +64 -0
  373. package/src/hooks/post/usePostUtils.ts +27 -0
  374. package/src/hooks/product/useProductPlanStore.ts +73 -0
  375. package/src/hooks/product/useProductStore.ts +64 -0
  376. package/src/hooks/product/useProductVersionStore.ts +57 -0
  377. package/src/hooks/site/useSiteStore.ts +53 -0
  378. package/src/hooks/tag/useTagStore.ts +65 -0
  379. package/src/hooks/utils/useAutosizeTextArea.ts +20 -0
  380. package/src/hooks/utils/useCountries.ts +25 -0
  381. package/src/hooks/utils/useGA4DataLayer.ts +25 -0
  382. package/src/hooks/utils/useNavOptions.ts +241 -0
  383. package/src/hooks/utils/useSearchAndFilterCommon.ts +78 -0
  384. package/src/hooks/utils/useTrackingEvents.ts +65 -0
  385. package/src/lib/app-paths.ts +19 -0
  386. package/src/lib/auth.ts +42 -0
  387. package/src/lib/next-router.ts +164 -0
  388. package/src/lib/routes.ts +36 -0
  389. package/src/lib/security.ts +19 -0
  390. package/src/pages/404.tsx +104 -0
  391. package/src/pages/500.tsx +26 -0
  392. package/src/pages/_app.tsx +87 -0
  393. package/src/pages/_document.tsx +26 -0
  394. package/src/pages/account/verify.tsx +178 -0
  395. package/src/pages/admin/activity/[activityId].tsx +77 -0
  396. package/src/pages/admin/activity/bans.tsx +197 -0
  397. package/src/pages/admin/activity/index.tsx +207 -0
  398. package/src/pages/admin/activity/settings.tsx +189 -0
  399. package/src/pages/admin/categories/index.tsx +61 -0
  400. package/src/pages/admin/coupons/index.tsx +38 -0
  401. package/src/pages/admin/entitlements/index.tsx +646 -0
  402. package/src/pages/admin/index.tsx +65 -0
  403. package/src/pages/admin/media/[id]/index.tsx +55 -0
  404. package/src/pages/admin/media/index.tsx +62 -0
  405. package/src/pages/admin/orgs/[orgId]/[name].tsx +1 -0
  406. package/src/pages/admin/orgs/index.tsx +256 -0
  407. package/src/pages/admin/plans/index.tsx +26 -0
  408. package/src/pages/admin/posts/[id].tsx +80 -0
  409. package/src/pages/admin/posts/index.tsx +63 -0
  410. package/src/pages/admin/products/[id].tsx +71 -0
  411. package/src/pages/admin/products/index.tsx +62 -0
  412. package/src/pages/admin/profile/index.tsx +26 -0
  413. package/src/pages/admin/providers/[id].tsx +77 -0
  414. package/src/pages/admin/providers/index.tsx +63 -0
  415. package/src/pages/admin/tags/index.tsx +58 -0
  416. package/src/pages/admin/users/[userId]/[name].tsx +1 -0
  417. package/src/pages/admin/users/index.tsx +295 -0
  418. package/src/pages/admin/versions/[id].tsx +211 -0
  419. package/src/pages/admin/versions/index.tsx +25 -0
  420. package/src/pages/blog/[postSlug].tsx +106 -0
  421. package/src/pages/blog/c/[categorySlug]/index.tsx +126 -0
  422. package/src/pages/blog/index.tsx +37 -0
  423. package/src/pages/cart/[productPlanSlug].tsx +631 -0
  424. package/src/pages/checkout/[productPlanSlug].tsx +368 -0
  425. package/src/pages/docs/index.tsx +24 -0
  426. package/src/pages/docs/sites/[siteId]/[siteName]/index.tsx +67 -0
  427. package/src/pages/docs/sites/[siteId]/[siteName]/pages/[pagesId].tsx +79 -0
  428. package/src/pages/docs/sites/[siteId]/[siteName]/pages/index.tsx +67 -0
  429. package/src/pages/docs/sites/[siteId]/[siteName]/preview/[[...slug]].tsx +7 -0
  430. package/src/pages/forgot.tsx +46 -0
  431. package/src/pages/groups/[groupId]/[name].tsx +337 -0
  432. package/src/pages/groups/index.tsx +62 -0
  433. package/src/pages/healthcheck.tsx +7 -0
  434. package/src/pages/index.tsx +309 -0
  435. package/src/pages/login.tsx +75 -0
  436. package/src/pages/marketplace/[slug].tsx +171 -0
  437. package/src/pages/marketplace/index.tsx +92 -0
  438. package/src/pages/openapi/index.tsx +77 -0
  439. package/src/pages/openapi/tokens/index.tsx +128 -0
  440. package/src/pages/org/[name]/settings.tsx +14 -0
  441. package/src/pages/org/[name]/token/index.tsx +109 -0
  442. package/src/pages/pricing.tsx +242 -0
  443. package/src/pages/projects/[projectId]/[name]/files/index.tsx +121 -0
  444. package/src/pages/projects/[projectId]/[name]/index.tsx +305 -0
  445. package/src/pages/projects/[projectId]/[name]/permission/index.tsx +143 -0
  446. package/src/pages/projects/[projectId]/token/index.tsx +120 -0
  447. package/src/pages/projects/index.tsx +129 -0
  448. package/src/pages/providers/[name].tsx +512 -0
  449. package/src/pages/providers/index.tsx +472 -0
  450. package/src/pages/reset.tsx +69 -0
  451. package/src/pages/settings/billing/index.tsx +41 -0
  452. package/src/pages/settings/index.tsx +133 -0
  453. package/src/pages/settings/organizations/[orgId]/invite/index.tsx +162 -0
  454. package/src/pages/settings/organizations/[orgId]/settings/index.tsx +98 -0
  455. package/src/pages/settings/organizations/[orgId]/token/index.tsx +109 -0
  456. package/src/pages/settings/organizations/index.tsx +148 -0
  457. package/src/pages/settings/organizations/new/index.tsx +252 -0
  458. package/src/pages/settings/profile/change-password.tsx +26 -0
  459. package/src/pages/settings/profile/index.tsx +103 -0
  460. package/src/pages/settings/profile/org-profile.tsx +127 -0
  461. package/src/pages/settings/profile/profile.tsx +26 -0
  462. package/src/pages/settings/sites/index.tsx +85 -0
  463. package/src/pages/settings/subscriptions/index.tsx +76 -0
  464. package/src/pages/settings/tokens/index.tsx +149 -0
  465. package/src/pages/signup/index.tsx +83 -0
  466. package/src/pages/signup/verify.tsx +138 -0
  467. package/src/pages/sites/[siteId]/[siteName]/[[...slug]].tsx +103 -0
  468. package/src/pages/sites/[siteId]/[siteName]/pages/[pageId].tsx +243 -0
  469. package/src/pages/sites/[siteId]/[siteName]/pages/index.tsx +140 -0
  470. package/src/pages/sites/host/[[...slug]].tsx +75 -0
  471. package/src/pages/storage/[vectorStoreId]/[name].tsx +653 -0
  472. package/src/pages/storage/index.tsx +526 -0
  473. package/src/pages/update/index.tsx +109 -0
  474. package/src/pages/update/verify.tsx +121 -0
  475. package/src/pages/users/[userId]/[name].tsx +791 -0
  476. package/src/pages/users/index.tsx +152 -0
  477. package/src/sass/app.scss +59 -0
  478. package/src/sass/base/_animations.scss +70 -0
  479. package/src/sass/base/_borders.scss +78 -0
  480. package/src/sass/base/_custom-backgrounds.scss +107 -0
  481. package/src/sass/base/_interactions.scss +35 -0
  482. package/src/sass/base/_main.scss +239 -0
  483. package/src/sass/base/_positions.scss +111 -0
  484. package/src/sass/base/_sizes.scss +120 -0
  485. package/src/sass/base/_typography.scss +184 -0
  486. package/src/sass/components/_accordion.scss +63 -0
  487. package/src/sass/components/_ad.scss +44 -0
  488. package/src/sass/components/_avatar.scss +18 -0
  489. package/src/sass/components/_badge.scss +6 -0
  490. package/src/sass/components/_button.scss +53 -0
  491. package/src/sass/components/_checkbox.scss +52 -0
  492. package/src/sass/components/_circle-media-picker.scss +32 -0
  493. package/src/sass/components/_code-snipped.scss +50 -0
  494. package/src/sass/components/_date-time-picker.scss +796 -0
  495. package/src/sass/components/_dialog.scss +27 -0
  496. package/src/sass/components/_gradients.scss +7 -0
  497. package/src/sass/components/_group-detail.scss +620 -0
  498. package/src/sass/components/_input.scss +567 -0
  499. package/src/sass/components/_menu.scss +31 -0
  500. package/src/sass/components/_modal.scss +273 -0
  501. package/src/sass/components/_popover.scss +29 -0
  502. package/src/sass/components/_rich-content.scss +531 -0
  503. package/src/sass/components/_spinner.scss +71 -0
  504. package/src/sass/components/_swagger.scss +7 -0
  505. package/src/sass/components/_swiper.scss +16 -0
  506. package/src/sass/components/_switch.scss +46 -0
  507. package/src/sass/components/_table.scss +39 -0
  508. package/src/sass/components/_tabs.scss +38 -0
  509. package/src/sass/components/_tag-input.scss +28 -0
  510. package/src/sass/components/_toast.scss +26 -0
  511. package/src/sass/components/_tooltip.scss +31 -0
  512. package/src/sass/layout/_base.scss +937 -0
  513. package/src/sass/layout/_footer.scss +148 -0
  514. package/src/sass/layout/_hero.scss +33 -0
  515. package/src/sass/layout/_nav.scss +77 -0
  516. package/src/sass/layout/_news-card-grid.scss +507 -0
  517. package/src/sass/utilities/_mixins.scss +21 -0
  518. package/src/sass/utilities/_sizes.scss +137 -0
  519. package/src/sass/utilities/_utils.scss +23 -0
  520. package/src/sass/utilities/_variables.scss +115 -0
  521. package/src/store/hooks.ts +5 -0
  522. package/src/store/hydrate.ts +1 -0
  523. package/src/store/model/activity.model.ts +52 -0
  524. package/src/store/model/address.model.ts +9 -0
  525. package/src/store/model/apiToken.model.ts +26 -0
  526. package/src/store/model/category.model.ts +20 -0
  527. package/src/store/model/entityStatus.model.ts +25 -0
  528. package/src/store/model/index.ts +25 -0
  529. package/src/store/model/invoice.model.ts +37 -0
  530. package/src/store/model/media.model.ts +70 -0
  531. package/src/store/model/org-group-permission.model.ts +16 -0
  532. package/src/store/model/org-group.model.ts +14 -0
  533. package/src/store/model/org-membership.model.ts +23 -0
  534. package/src/store/model/org.model.ts +13 -0
  535. package/src/store/model/page.model.ts +33 -0
  536. package/src/store/model/post.model.ts +60 -0
  537. package/src/store/model/product-coupon.model.ts +42 -0
  538. package/src/store/model/product-feature-entitlement.ts +40 -0
  539. package/src/store/model/product-feature.ts +29 -0
  540. package/src/store/model/product-plan.model.ts +120 -0
  541. package/src/store/model/product-version.model.ts +24 -0
  542. package/src/store/model/product.model.ts +91 -0
  543. package/src/store/model/project-file.model.ts +18 -0
  544. package/src/store/model/project-user-permission.model.ts +41 -0
  545. package/src/store/model/project.model.ts +48 -0
  546. package/src/store/model/provider.model.ts +118 -0
  547. package/src/store/model/purchase.model.ts +19 -0
  548. package/src/store/model/site-crawl-job.model.ts +31 -0
  549. package/src/store/model/site-page.model.ts +37 -0
  550. package/src/store/model/site.model.ts +15 -0
  551. package/src/store/model/subscription.model.ts +60 -0
  552. package/src/store/model/tag.model.ts +18 -0
  553. package/src/store/model/user-permission.model.ts +21 -0
  554. package/src/store/model/user.model.ts +97 -0
  555. package/src/store/model/vector-store-file.model.ts +40 -0
  556. package/src/store/model/vector-store.model.ts +41 -0
  557. package/src/store/slice/api/accountApiSlice.ts +92 -0
  558. package/src/store/slice/api/accountSlice.ts +57 -0
  559. package/src/store/slice/api/activityApiSlice.ts +218 -0
  560. package/src/store/slice/api/apiTokenSlice.ts +153 -0
  561. package/src/store/slice/api/authApiSlice.ts +220 -0
  562. package/src/store/slice/api/base-domain-api.ts +19 -0
  563. package/src/store/slice/api/billingUnitTypesApiSlice.ts +45 -0
  564. package/src/store/slice/api/bootstrapIconApiSlice.ts +36 -0
  565. package/src/store/slice/api/categoriesApiSlice.ts +101 -0
  566. package/src/store/slice/api/fontawesomeApiSlice.ts +43 -0
  567. package/src/store/slice/api/invoiceApiSlice.ts +37 -0
  568. package/src/store/slice/api/leadApiSlice.ts +48 -0
  569. package/src/store/slice/api/mediaApiSlice.ts +276 -0
  570. package/src/store/slice/api/orgApiSlice.ts +185 -0
  571. package/src/store/slice/api/orgGroupApiSlice.ts +146 -0
  572. package/src/store/slice/api/pageApiSlice.ts +111 -0
  573. package/src/store/slice/api/paymentApiSlice.ts +27 -0
  574. package/src/store/slice/api/postApiSlice.ts +369 -0
  575. package/src/store/slice/api/productCouponsApiSlice.ts +101 -0
  576. package/src/store/slice/api/productFeaturesApiSlice.ts +93 -0
  577. package/src/store/slice/api/productPlansApiSlice.ts +142 -0
  578. package/src/store/slice/api/productSubscriptionsApiSlice.ts +156 -0
  579. package/src/store/slice/api/productVersionsApiSlice.ts +116 -0
  580. package/src/store/slice/api/productsApiSlice.ts +169 -0
  581. package/src/store/slice/api/projectApiSlice.ts +135 -0
  582. package/src/store/slice/api/projectFileApiSlice.ts +152 -0
  583. package/src/store/slice/api/projectUserPermissionApiSlice.ts +153 -0
  584. package/src/store/slice/api/providerApiSlice.ts +259 -0
  585. package/src/store/slice/api/providerSettingApiSlice.ts +174 -0
  586. package/src/store/slice/api/siteApiSlice.ts +93 -0
  587. package/src/store/slice/api/stripeApiSlice.ts +114 -0
  588. package/src/store/slice/api/tagsApiSlice.ts +96 -0
  589. package/src/store/slice/api/userApiSlice.ts +305 -0
  590. package/src/store/slice/api/vectorStoreApiSlice.ts +119 -0
  591. package/src/store/slice/api/vectorStoreFileApiSlice.ts +127 -0
  592. package/src/store/store.ts +130 -0
  593. package/src/types/bootstrap.ts +30 -0
  594. package/tsconfig.json +103 -0
@@ -0,0 +1,1016 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useRef, ReactNode } from 'react'
4
+ import { createPortal } from 'react-dom'
5
+ import { useRouter } from '@lib/next-router'
6
+ import { useUserAuthContext } from '@context/UserAuthContext'
7
+ import Link from 'next/link'
8
+ import Button from '@components/common/button/Button'
9
+ import { canAccessApp } from '@helpers/permissions'
10
+ import { useListOrgsQuery, useSetActiveOrgMutation } from '@store/slice/api/orgApiSlice'
11
+ import { useAppDispatch } from '@store/hooks'
12
+ import { projectApiSlice } from '@store/slice/api/projectApiSlice'
13
+ import { vectorStoreApiSlice } from '@store/slice/api/vectorStoreApiSlice'
14
+ import { vectorStoreFileApiSlice } from '@store/slice/api/vectorStoreFileApiSlice'
15
+ import { projectFileApiSlice } from '@store/slice/api/projectFileApiSlice'
16
+ import { projectUserPermissionsApiSlice } from '@store/slice/api/projectUserPermissionApiSlice'
17
+ import { stripeApiSlice } from '@store/slice/api/stripeApiSlice'
18
+ import { invoiceApiSlice } from '@store/slice/api/invoiceApiSlice'
19
+ import { siteApiSlice } from '@store/slice/api/siteApiSlice'
20
+ import { pageApiSlice } from '@store/slice/api/pageApiSlice'
21
+ import { apiTokenApiSlice } from '@store/slice/api/apiTokenSlice'
22
+ import { productsApiSlice } from '@store/slice/api/productsApiSlice'
23
+ import { OptimizedImage } from '@components/common/Image/OptimizedImage'
24
+ import { colorCirclePNG } from '@helpers/images'
25
+
26
+ type NavItem = {
27
+ label: string
28
+ icon?: ReactNode | string // ReactNode OR bootstrap icon class OR image URL
29
+ title?: string
30
+ href?: string
31
+ onClick?: () => void
32
+ external?: boolean
33
+ activeHrefs?: string[]
34
+ }
35
+
36
+ type MainNavMode = 'auto' | 'list' | 'select'
37
+ type NavAppKey = 'projects' | 'storage' | 'users' | 'providers'
38
+
39
+ type SideNavLayoutProps = {
40
+ children: ReactNode
41
+ sidebarTopContent?: (isCollapsed: boolean, setIsCollapsed: (v: boolean) => void) => ReactNode
42
+ sidebarContent?: (isCollapsed: boolean, setIsCollapsed: (v: boolean) => void) => ReactNode
43
+ mainNavTitle?: string
44
+ mainNavItems?: NavItem[]
45
+ renderMainNav?: (isCollapsed: boolean, setIsCollapsed: (v: boolean) => void) => ReactNode
46
+ /** controls Explore rendering – 'auto' = homepage list, others select */
47
+ mainNavMode?: MainNavMode
48
+ /** toggle id="mainContentArea" on non-blog content wrapper */
49
+ useMainContentAreaId?: boolean
50
+ }
51
+
52
+ const DEFAULT_ITEMS: NavItem[] = [
53
+ { label: 'Storage', icon: <i className="bi bi-grid-3x3-gap-fill" />, title: 'Stores', href: '/storage' },
54
+ { label: 'Projects', icon: <i className="bi bi-kanban" />, title: 'Projects', href: '/projects' },
55
+ { label: 'Providers', icon: <i className="bi bi-building" />, title: 'Providers', href: '/providers' },
56
+ //{ label: 'Docs', icon: <i className="bi bi-journal-text" />, title: 'Docs', href: '/docs' },
57
+ { label: 'Users', icon: <i className="bi bi-people" />, title: 'Users', href: '/users' },
58
+ { label: 'Groups', icon: <i className="bi bi-diagram-3" />, title: 'Groups', href: '/groups' },
59
+ // { label: 'Api', icon: <i className="bi bi-motherboard" />, title: 'Api', href: '/openapi' },
60
+ ]
61
+
62
+ const SideNavLayout = ({
63
+ children,
64
+ sidebarTopContent,
65
+ sidebarContent,
66
+ mainNavTitle = 'Apps',
67
+ mainNavItems,
68
+ renderMainNav,
69
+ mainNavMode = 'auto',
70
+ useMainContentAreaId = true
71
+ }: SideNavLayoutProps) => {
72
+ const { user, isConnected, activeOrgId, setActiveOrgId } = useUserAuthContext()
73
+ const isAuthenticated = !!user?.id
74
+ const router = useRouter()
75
+ const dispatch = useAppDispatch()
76
+ const { data: orgMembershipsData } = useListOrgsQuery(undefined, {
77
+ skip: !isAuthenticated || !isConnected,
78
+ refetchOnMountOrArgChange: true,
79
+ })
80
+ const [setActiveOrg, { isLoading: isSettingOrg }] = useSetActiveOrgMutation()
81
+
82
+ const isSlugPage = !!router.query.slug
83
+ const isBlogPage = router.pathname === '/blog' || router.pathname.startsWith('/blog/')
84
+ const isMainBlogPage = router.pathname === '/blog'
85
+ const isBlogDetailPage = router.pathname === '/blog/[postSlug]'
86
+ const isSettingsRoute = router.pathname === '/settings' || router.pathname.startsWith('/settings/')
87
+ const mainContentAreaIdProps = useMainContentAreaId ? { id: 'mainContentArea' } : {}
88
+
89
+ const [isCollapsed, setIsCollapsed] = useState(false)
90
+ const [isMobile, setIsMobile] = useState(false)
91
+ const [isMobileOpen, setIsMobileOpen] = useState(false)
92
+ const [hasMarketplaceHeader, setHasMarketplaceHeader] = useState(false)
93
+
94
+ // --- Select-style Explore state ---
95
+ const [isOpen, setIsOpen] = useState(false)
96
+ const [menuDir, setMenuDir] = useState<'up' | 'down'>('down')
97
+ const [menuMaxHeight, setMenuMaxHeight] = useState<number>(240)
98
+ const [menuPos, setMenuPos] = useState<{ left: number; top?: number; bottom?: number; width: number }>({ left: 0, top: 0, width: 200 })
99
+ const triggerRef = useRef<HTMLButtonElement | null>(null)
100
+ const menuRef = useRef<HTMLDivElement | null>(null)
101
+ const sidebarRef = useRef<HTMLDivElement | null>(null)
102
+
103
+ // NEW: navigation lock + pending selection
104
+ const [navPendingHref, setNavPendingHref] = useState<string | null>(null)
105
+ const [navPendingLabel, setNavPendingLabel] = useState<string | null>(null)
106
+
107
+ // Org picker dropdown state
108
+ const [isOrgPickerOpen, setIsOrgPickerOpen] = useState(false)
109
+ const orgPickerRef = useRef<HTMLButtonElement | null>(null)
110
+ const orgMenuRef = useRef<HTMLDivElement | null>(null)
111
+
112
+ // Watch screen size to determine mobile mode
113
+ useEffect(() => {
114
+ const handleResize = () => {
115
+ const mobile = window.innerWidth < 768
116
+ setIsMobile(mobile)
117
+ if (!mobile) setIsMobileOpen(false)
118
+ }
119
+ handleResize()
120
+ window.addEventListener('resize', handleResize)
121
+ return () => window.removeEventListener('resize', handleResize)
122
+ }, [])
123
+
124
+ useEffect(() => {
125
+ const marketplaceHeader =
126
+ document.querySelector('.bg-marketplace-header') ||
127
+ document.getElementById('bg-marketplace-header')
128
+ setHasMarketplaceHeader(!!marketplaceHeader)
129
+ }, [])
130
+
131
+ const itemsToRender = mainNavItems && mainNavItems.length > 0 ? mainNavItems : DEFAULT_ITEMS
132
+ const permissionRows = user?.permissions
133
+ const orgPermissionRows = Array.isArray(permissionRows)
134
+ ? (activeOrgId ? permissionRows.filter((row) => row.orgId === activeOrgId) : [])
135
+ : []
136
+ const hasPermissionConfig = orgPermissionRows.length > 0
137
+ const hasOrgMembershipsFromUser = Array.isArray(user?.orgMemberships)
138
+ const orgMemberships = orgMembershipsData ?? (hasOrgMembershipsFromUser ? user?.orgMemberships : []) ?? []
139
+ const orgMembershipsResolved = !!orgMembershipsData || hasOrgMembershipsFromUser
140
+ const activeMembership =
141
+ orgMemberships.find((m) => m.orgId === activeOrgId) ||
142
+ orgMemberships.find((m) => m.org?.orgType === 'personal') ||
143
+ orgMemberships[0]
144
+ const activeOrg = activeMembership?.org
145
+ const isPersonalOrgByOrg = (org?: any) => {
146
+ if (!org) return false
147
+ const slug = String(org.slug || '').trim().toLowerCase()
148
+ const name = String(org.name || '').trim()
149
+ const username = String(user?.name || '').trim().toLowerCase()
150
+ return (
151
+ org?.orgType === 'personal' ||
152
+ (!!slug && slug.endsWith('-personal')) ||
153
+ (!!name && /\(personal\b/i.test(name)) ||
154
+ (!!slug && !!username && slug === username)
155
+ )
156
+ }
157
+ const isPersonalOrg = isPersonalOrgByOrg(activeOrg)
158
+ const isBusinessOrg = activeOrg?.orgType === 'business'
159
+ const isOrgOwner =
160
+ !!user?.id &&
161
+ !!activeOrg?.ownerId &&
162
+ String(activeOrg.ownerId).trim() === String(user.id).trim()
163
+ const shouldHideUsersInNav = !orgMembershipsResolved || !orgMemberships.length || isPersonalOrg
164
+ const NAV_APP_MAP: Record<string, NavAppKey> = {
165
+ '/projects': 'projects',
166
+ '/storage': 'storage',
167
+ '/users': 'users',
168
+ '/groups': 'users',
169
+ '/providers': 'providers',
170
+ }
171
+ const normalizeNavHref = (href: string) => {
172
+ const raw = href.split('#')[0].split('?')[0]
173
+ if (raw === '/') return '/'
174
+ return raw.endsWith('/') ? raw.slice(0, -1) : raw
175
+ }
176
+
177
+ const filteredItems = itemsToRender.filter((item) => {
178
+ if (!item.href) return true
179
+ const normalizedHref = normalizeNavHref(item.href)
180
+ const appKey = NAV_APP_MAP[normalizedHref]
181
+ if (!appKey) return false
182
+ if ((normalizedHref === '/users' || normalizedHref === '/groups') && shouldHideUsersInNav) return false
183
+ if (isOrgOwner) return true
184
+ if (isBusinessOrg && !hasPermissionConfig) return false
185
+ if (!hasPermissionConfig) return true
186
+ const hasAppRows = orgPermissionRows.some((row: any) => row.appKey === appKey)
187
+ if (isBusinessOrg && !hasAppRows) return false
188
+ if (!hasAppRows) return true
189
+ return canAccessApp(orgPermissionRows, appKey, 'read')
190
+ })
191
+
192
+ const currentPath = (() => {
193
+ const raw = (router.asPath || router.pathname || '/').split('#')[0].split('?')[0]
194
+ if (raw === '/') return '/'
195
+ return raw.endsWith('/') ? raw.slice(0, -1) : raw
196
+ })()
197
+ const currentPathSegments = currentPath.split('/').filter(Boolean)
198
+ const detailScopedApps = new Set(['projects', 'groups'])
199
+ const firstPathSegment = currentPathSegments[0] || ''
200
+ const secondPathSegment = currentPathSegments[1] || ''
201
+ const isRootResourceDetailRoute =
202
+ detailScopedApps.has(firstPathSegment) &&
203
+ currentPathSegments.length > 1 &&
204
+ secondPathSegment !== 'public'
205
+ const shouldHideOrgPicker = isRootResourceDetailRoute
206
+
207
+ const isItemActive = (item?: NavItem) => {
208
+ const href = item?.href
209
+ if (!href) return false
210
+ const activeHrefs = item.activeHrefs || [href]
211
+ return activeHrefs.some((activeHref) => {
212
+ const normalizedHref = activeHref === '/' ? '/' : activeHref.replace(/\/+$/, '')
213
+ if (normalizedHref === '/') return currentPath === '/'
214
+ return currentPath === normalizedHref || currentPath.startsWith(`${normalizedHref}/`)
215
+ })
216
+ }
217
+
218
+ const resetOrgScopedCaches = () => {
219
+ dispatch(projectApiSlice.util.resetApiState())
220
+ dispatch(vectorStoreApiSlice.util.resetApiState())
221
+ dispatch(vectorStoreFileApiSlice.util.resetApiState())
222
+ dispatch(projectFileApiSlice.util.resetApiState())
223
+ dispatch(projectUserPermissionsApiSlice.util.resetApiState())
224
+ // Settings-scoped caches
225
+ dispatch(stripeApiSlice.util.resetApiState())
226
+ dispatch(invoiceApiSlice.util.resetApiState())
227
+ dispatch(siteApiSlice.util.resetApiState())
228
+ dispatch(pageApiSlice.util.resetApiState())
229
+ dispatch(apiTokenApiSlice.util.resetApiState())
230
+ dispatch(productsApiSlice.util.resetApiState())
231
+ }
232
+
233
+ useEffect(() => {
234
+ if (!isAuthenticated) return
235
+ if (activeOrgId || !activeMembership?.orgId) return
236
+ setActiveOrg({ orgId: activeMembership.orgId })
237
+ .unwrap()
238
+ .then((res) => setActiveOrgId(res.orgId))
239
+ .catch(() => {})
240
+ }, [activeMembership?.orgId, activeOrgId, isAuthenticated, setActiveOrg, setActiveOrgId])
241
+
242
+ const sidebarClass = [
243
+ 'sidebar-column',
244
+ isCollapsed ? 'collapsed' : '',
245
+ isMobile ? 'position-fixed top-0 start-0 h-100 bg-white' : 'position-relative',
246
+ isMobile && !isMobileOpen ? 'd-none' : '',
247
+ ]
248
+ .filter(Boolean)
249
+ .join(' ')
250
+
251
+ // 1) Replace your current pushRoute with this:
252
+ const pushRoute = (href: string) => {
253
+ // menu should already be closed; just navigate on next frame
254
+ // (rAF helps ensure the close renders before route change)
255
+ requestAnimationFrame(() => {
256
+ // setTimeout as a Safari fallback micro-delay
257
+ setTimeout(() => router.push(href), 0)
258
+ })
259
+ }
260
+
261
+ // 2) Update handleItemClick to close the menu BEFORE navigating:
262
+ const handleItemClick = (item: NavItem) => {
263
+ if (navPendingHref) return // locked
264
+ if (item.onClick) {
265
+ item.onClick()
266
+ return
267
+ }
268
+ if (item.href) {
269
+ if (item.external) {
270
+ window.open(item.href, '_blank', 'noopener,noreferrer')
271
+ } else {
272
+ // lock + show spinner in trigger
273
+ setNavPendingHref(item.href)
274
+ setNavPendingLabel(item.label)
275
+
276
+ // close dropdown first, then navigate
277
+ setIsOpen(false)
278
+ pushRoute(item.href)
279
+ }
280
+ }
281
+ }
282
+
283
+ // Clear pending when route completes/errors
284
+ useEffect(() => {
285
+ const done = () => {
286
+ setNavPendingHref(null)
287
+ setNavPendingLabel(null)
288
+ setIsOpen(false)
289
+ }
290
+ const start = () => {
291
+ // ensure menu stays locked if it remains visible during transition
292
+ }
293
+ router.events.on('routeChangeStart', start)
294
+ router.events.on('routeChangeComplete', done)
295
+ router.events.on('routeChangeError', done)
296
+ return () => {
297
+ router.events.off('routeChangeStart', start)
298
+ router.events.off('routeChangeComplete', done)
299
+ router.events.off('routeChangeError', done)
300
+ }
301
+ }, [router.events])
302
+
303
+ const handleLogoClick = (e: React.MouseEvent) => {
304
+ e.preventDefault()
305
+ if (router.pathname === '/') {
306
+ router.reload()
307
+ } else {
308
+ router.push('/')
309
+ }
310
+ }
311
+
312
+ // --- Explore Select helpers ---
313
+ const isImgUrl = (s?: string) => !!s && /^(\/|https?:\/\/|data:image)/i.test(s)
314
+ const isBiClass = (s?: string) => !!s && /\bbi\b/.test(s)
315
+
316
+ const renderThumb = (icon?: NavItem['icon']) => {
317
+ if (!icon) return <span aria-hidden>🧠</span>
318
+ if (typeof icon !== 'string') return icon
319
+ if (isImgUrl(icon)) {
320
+ return <img src={icon} alt="" style={{ width: 30, height: 30, objectFit: 'cover', borderRadius: 6 }} />
321
+ }
322
+ if (isBiClass(icon)) return <i className={icon} />
323
+ return <span>{icon}</span>
324
+ }
325
+
326
+ const selectedOption = filteredItems.find((item) => isItemActive(item)) || filteredItems[0]
327
+
328
+ // Decide up/down via TRIGGER, but set left/width to SIDEBAR (=> 100% width)
329
+ const computeMenuPlacement = () => {
330
+ const btn = triggerRef.current
331
+ const sidebarEl = sidebarRef.current
332
+ if (!btn || !sidebarEl) return
333
+
334
+ const btnRect = btn.getBoundingClientRect()
335
+ const hostRect = sidebarEl.getBoundingClientRect()
336
+ const vh = window.innerHeight
337
+ const vw = window.innerWidth
338
+
339
+ const below = vh - btnRect.bottom - 8
340
+ const above = btnRect.top - 8
341
+ const minMenu = 200
342
+ const dir: 'up' | 'down' = below < minMenu && above > below ? 'up' : 'down'
343
+ setMenuDir(dir)
344
+
345
+ const maxH = Math.max(180, Math.floor((dir === 'down' ? below : above) - 8))
346
+ setMenuMaxHeight(maxH)
347
+
348
+ const width = Math.min(hostRect.width, vw - 16)
349
+ let left = Math.round(hostRect.left + window.scrollX)
350
+ left = Math.max(8, Math.min(left, vw - width - 8))
351
+
352
+ if (dir === 'down') {
353
+ const top = Math.round(btnRect.bottom + window.scrollY + 8)
354
+ setMenuPos({ left, top, width })
355
+ } else {
356
+ const bottom = Math.round(vh - btnRect.top + 8)
357
+ setMenuPos({ left, bottom, width })
358
+ }
359
+ }
360
+
361
+ useEffect(() => {
362
+ if (!isOpen) return
363
+ const onScrollOrResize = () => computeMenuPlacement()
364
+ const onClickAway = (e: MouseEvent) => {
365
+ if (navPendingHref) return // locked; ignore outside clicks
366
+ if (triggerRef.current?.contains(e.target as Node)) return
367
+ if (menuRef.current?.contains(e.target as Node)) return
368
+ setIsOpen(false)
369
+ }
370
+ const onKey = (e: KeyboardEvent) => {
371
+ if (navPendingHref) return
372
+ if (e.key === 'Escape') setIsOpen(false)
373
+ }
374
+ window.addEventListener('resize', onScrollOrResize)
375
+ window.addEventListener('scroll', onScrollOrResize, true)
376
+ window.addEventListener('click', onClickAway)
377
+ window.addEventListener('keydown', onKey)
378
+ computeMenuPlacement()
379
+ return () => {
380
+ window.removeEventListener('resize', onScrollOrResize)
381
+ window.removeEventListener('scroll', onScrollOrResize, true)
382
+ window.removeEventListener('click', onClickAway)
383
+ window.removeEventListener('keydown', onKey)
384
+ }
385
+ }, [isOpen, navPendingHref])
386
+
387
+ // Org picker outside click handler
388
+ useEffect(() => {
389
+ if (!isOrgPickerOpen) return
390
+ const onClickAway = (e: MouseEvent) => {
391
+ if (orgPickerRef.current?.contains(e.target as Node)) return
392
+ if (orgMenuRef.current?.contains(e.target as Node)) return
393
+ setIsOrgPickerOpen(false)
394
+ }
395
+ const onKey = (e: KeyboardEvent) => {
396
+ if (e.key === 'Escape') setIsOrgPickerOpen(false)
397
+ }
398
+ window.addEventListener('click', onClickAway)
399
+ window.addEventListener('keydown', onKey)
400
+ return () => {
401
+ window.removeEventListener('click', onClickAway)
402
+ window.removeEventListener('keydown', onKey)
403
+ }
404
+ }, [isOrgPickerOpen])
405
+
406
+ const onOpenToggle = () => {
407
+ if (navPendingHref) return // locked
408
+ if (!isOpen) computeMenuPlacement()
409
+ setIsOpen(v => !v)
410
+ }
411
+
412
+ // Keyboard navigation within menu (portal)
413
+ const onMenuKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
414
+ if (navPendingHref) {
415
+ e.preventDefault()
416
+ return
417
+ }
418
+ const root = menuRef.current || document.body
419
+ const items = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-option="true"]'))
420
+ const idx = items.findIndex(el => el === document.activeElement)
421
+ if (e.key === 'ArrowDown') {
422
+ e.preventDefault()
423
+ const next = items[(idx + 1 + items.length) % items.length]
424
+ next?.focus()
425
+ } else if (e.key === 'ArrowUp') {
426
+ e.preventDefault()
427
+ const prev = items[(idx - 1 + items.length) % items.length]
428
+ prev?.focus()
429
+ }
430
+ }
431
+
432
+ // Get icon for org membership
433
+ const getOrgIcon = (membership: typeof activeMembership) => {
434
+ if (!membership) return colorCirclePNG
435
+ const org = membership.org
436
+ const isPersonal = isPersonalOrgByOrg(org)
437
+
438
+ return isPersonal
439
+ ? user?.profileMedia?.media?.sourceUrl || colorCirclePNG
440
+ : org?.profileMedia?.media?.sourceUrl ||
441
+ org?.profileImageUrl ||
442
+ colorCirclePNG
443
+ }
444
+
445
+ const isPersonalMembership = (membership: typeof activeMembership) => {
446
+ if (!membership) return false
447
+ const org = membership.org
448
+ return isPersonalOrgByOrg(org)
449
+ }
450
+
451
+ const getOrgDisplayName = (membership: typeof activeMembership) => {
452
+ if (!membership) return 'Organization'
453
+ const fallbackName = (membership.org?.name || 'Organization').replace(/\s*\(Personal\)\s*$/i, '')
454
+ if (!isPersonalMembership(membership)) return fallbackName
455
+ const username = String((user as any)?.username || user?.name || '').trim()
456
+ if (!username) return fallbackName
457
+ return username.charAt(0).toUpperCase() + username.slice(1)
458
+ }
459
+
460
+ // Handle org change
461
+ const handleOrgChange = async (nextOrgId: string) => {
462
+ if (!nextOrgId || nextOrgId === activeOrgId || isSettingOrg) return
463
+ setIsOrgPickerOpen(false)
464
+ try {
465
+ const result = await setActiveOrg({ orgId: nextOrgId }).unwrap()
466
+ setActiveOrgId(result.orgId)
467
+ resetOrgScopedCaches()
468
+ router.replace(router.asPath)
469
+ } catch {
470
+ // ignore for now; UI stays on current org
471
+ }
472
+ }
473
+
474
+ // --- Resolve which Explore variant to show
475
+ const hasSupplementalSidebarSections = !!(sidebarTopContent || sidebarContent)
476
+ const resolvedMainNavMode: MainNavMode =
477
+ mainNavMode !== 'auto'
478
+ ? mainNavMode
479
+ : (router.pathname === '/' || !hasSupplementalSidebarSections ? 'list' : 'select')
480
+
481
+ // Original LIST variant
482
+ const renderExploreList = () => (
483
+ <>
484
+ <h2 className="mt-4 sidebar-heading sidebar-nav-heading">{mainNavTitle}</h2>
485
+ <ul className="sidebar-list sidebar-nav-list">
486
+ {filteredItems.map((item, idx) => {
487
+ const isActive = isItemActive(item)
488
+ return (
489
+ <li
490
+ key={`${item.label}-${idx}`}
491
+ className={`sidebar-item sidebar-nav-item ${isActive ? 'active' : ''}`}
492
+ onClick={() => handleItemClick(item)}
493
+ title={item.title || item.label}
494
+ role="button"
495
+ aria-current={isActive ? 'page' : undefined}
496
+ style={isActive ? { background: 'var(--bs-alice-blue, #eff5fb)' } : undefined}
497
+ >
498
+ <span
499
+ className="sidebar-link sidebar-nav-link"
500
+ style={isActive ? { color: 'var(--bs-primary, #0d6efd)', fontWeight: 700 } : undefined}
501
+ >
502
+ <span className="icon-fw d-inline-flex align-items-center justify-content-center">
503
+ {typeof item.icon === 'string'
504
+ ? (/\bbi\b/.test(item.icon) ? <i className={item.icon} /> : <span>{item.icon}</span>)
505
+ : item.icon}
506
+ </span>
507
+ {!isCollapsed && <span className="ms-2">{item.label}</span>}
508
+ </span>
509
+ </li>
510
+ )
511
+ })}
512
+ </ul>
513
+ </>
514
+ )
515
+
516
+ // Select (portal) variant
517
+ const renderExploreSelect = () => {
518
+ // what to show in the trigger while navigating
519
+ const triggerLabel = navPendingLabel ?? selectedOption?.label ?? 'Select'
520
+ const triggerSpinning = !!navPendingHref
521
+
522
+ return (
523
+ <>
524
+ <h2 className="mt-5 sidebar-heading sidebar-nav-heading">{mainNavTitle}</h2>
525
+
526
+ {/* Trigger (collapses to 38x38 icon-only) */}
527
+ <button
528
+ ref={triggerRef}
529
+ type="button"
530
+ className="form-select form-select-sm sidebar-nav-trigger border-0 border-right-1"
531
+ style={{
532
+ minWidth: isCollapsed ? 38 : '100px',
533
+ width: isCollapsed ? 38 : '100%',
534
+ height: 38,
535
+ textAlign: 'left',
536
+ paddingRight: isCollapsed ? 0 : '2.3rem',
537
+ paddingLeft: isCollapsed ? 0 : '0.75rem',
538
+ backgroundImage: isCollapsed || triggerSpinning
539
+ ? 'none'
540
+ : "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 6 7 7 7-7'/%3e%3c/svg%3e\")",
541
+ backgroundRepeat: 'no-repeat',
542
+ backgroundPosition: 'right 0.4rem center',
543
+ backgroundSize: '16px 12px',
544
+ borderRadius: 10,
545
+ boxShadow: 'inset 0 0 0 1px rgba(0,0,0,.1)',
546
+ display: 'flex',
547
+ alignItems: 'center',
548
+ justifyContent: isCollapsed ? 'center' : 'flex-start',
549
+ gap: isCollapsed ? 0 : '0.5rem',
550
+ cursor: triggerSpinning ? 'progress' : 'pointer',
551
+ opacity: triggerSpinning ? 0.9 : 1
552
+ }}
553
+ onClick={onOpenToggle}
554
+ aria-expanded={isOpen}
555
+ aria-haspopup="listbox"
556
+ aria-busy={triggerSpinning || undefined}
557
+ aria-disabled={triggerSpinning || undefined}
558
+ aria-label={isCollapsed ? triggerLabel : undefined}
559
+ title={isCollapsed ? triggerLabel : undefined}
560
+ >
561
+ <span className="d-flex align-items-center">
562
+ <span className={isCollapsed ? '' : 'me-2'}>
563
+ {triggerSpinning ? (
564
+ <span className="spinner-border spinner-border-sm" role="status" aria-label="Loading" />
565
+ ) : (
566
+ // show selected/pending icon
567
+ renderThumb(
568
+ (navPendingHref
569
+ ? filteredItems.find(i => i.href === navPendingHref)?.icon
570
+ : selectedOption?.icon)
571
+ )
572
+ )}
573
+ </span>
574
+ {!isCollapsed && <span>{triggerLabel}</span>}
575
+ </span>
576
+ </button>
577
+
578
+ {/* Menu (PORTAL, 100% of sidebar) */}
579
+ {isOpen && createPortal(
580
+ <div
581
+ ref={menuRef}
582
+ role="listbox"
583
+ tabIndex={-1}
584
+ className={`shadow-sm rounded-3 border bg-white explore-menu-portal ${navPendingHref ? 'locked' : ''}`}
585
+ style={{
586
+ position: 'fixed',
587
+ zIndex: 3000,
588
+ left: menuPos.left,
589
+ top: menuPos.top,
590
+ bottom: menuPos.bottom,
591
+ width: menuPos.width,
592
+ maxHeight: menuMaxHeight,
593
+ overflowY: 'auto',
594
+ padding: '4px'
595
+ }}
596
+ onKeyDown={onMenuKeyDown}
597
+ aria-busy={!!navPendingHref || undefined}
598
+ aria-disabled={!!navPendingHref || undefined}
599
+ >
600
+ {filteredItems.map((item, idx) => {
601
+ const isActive = isItemActive(item)
602
+ const isPending = !!navPendingHref && item.href === navPendingHref
603
+ const disabled = !!navPendingHref
604
+ return (
605
+ <button
606
+ key={`${item.label}-${idx}`}
607
+ type="button"
608
+ role="option"
609
+ tabIndex={disabled ? -1 : 0}
610
+ data-option="true"
611
+ className={`w-100 text-start px-3 py-2 bg-transparent border-0 d-flex align-items-center explore-option ${isActive ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
612
+ onClick={() => {
613
+ if (disabled) return
614
+ setIsOpen(false)
615
+ handleItemClick(item)
616
+ }}
617
+ onKeyDown={(e) => {
618
+ if (disabled) { e.preventDefault(); return }
619
+ if (e.key === 'Enter' || e.key === ' ') {
620
+ e.preventDefault()
621
+ setIsOpen(false)
622
+ handleItemClick(item)
623
+ }
624
+ }}
625
+ title={item.title || item.label}
626
+ aria-selected={isActive || isPending}
627
+ aria-disabled={disabled || undefined}
628
+ >
629
+ <span className="me-2 thumb">
630
+ {isPending ? (
631
+ <span className="spinner-border spinner-border-sm" role="status" aria-label="Loading" />
632
+ ) : (
633
+ renderThumb(item.icon)
634
+ )}
635
+ </span>
636
+ <span className="flex-grow-1 label">{item.label}</span>
637
+ {(isActive || isPending) && !isPending && <i className="bi bi-check2" aria-hidden />}
638
+ </button>
639
+ )
640
+ })}
641
+ </div>,
642
+ document.body
643
+ )}
644
+
645
+ <ul className="list-unstyled mt-2 mb-0 text-muted small text-center" aria-hidden={!!navPendingHref}>
646
+ <li>Press ↑/↓ to navigate</li>
647
+ </ul>
648
+
649
+ {/* Global so it affects the portal */}
650
+ <style jsx global>{`
651
+ .sidebar-nav-heading {
652
+ color: var(--bs-dark-900, #111117);
653
+ font-size: .875rem;
654
+ font-weight: 700;
655
+ line-height: 1.25;
656
+ text-transform: none;
657
+ letter-spacing: normal;
658
+ margin-bottom: .5rem;
659
+ padding: 0 .75rem;
660
+ }
661
+ .sidebar-nav-list .sidebar-nav-item {
662
+ border-radius: .375rem;
663
+ margin-bottom: .1rem;
664
+ transition: background-color .12s ease, color .12s ease;
665
+ }
666
+ .sidebar-nav-list .sidebar-nav-item:hover,
667
+ .sidebar-nav-list .sidebar-nav-item.active {
668
+ background: var(--bs-alice-blue, #eff5fb);
669
+ }
670
+ .sidebar-nav-list .sidebar-nav-item.active .sidebar-nav-link {
671
+ color: var(--bs-primary, #0d6efd);
672
+ font-weight: 700;
673
+ }
674
+ .sidebar-nav-list .sidebar-nav-link {
675
+ color: var(--bs-dark-900, #111117);
676
+ font-size: .95rem;
677
+ font-weight: 600;
678
+ line-height: 1.2;
679
+ min-height: 2.25rem;
680
+ padding: .5rem .75rem;
681
+ }
682
+ .sidebar-nav-trigger {
683
+ color: var(--bs-dark-900, #111117) !important;
684
+ font-size: .95rem !important;
685
+ font-weight: 600 !important;
686
+ }
687
+ .explore-menu-portal .explore-option {
688
+ border-radius: .5rem;
689
+ margin: 2px 0;
690
+ color: var(--bs-dark-900, #111117);
691
+ font-size: .95rem;
692
+ font-weight: 600;
693
+ outline: none;
694
+ transition: background-color .12s ease, color .12s ease, box-shadow .12s ease, opacity .12s ease;
695
+ }
696
+ .explore-menu-portal:not(.locked) .explore-option:hover,
697
+ .explore-menu-portal:not(.locked) .explore-option:focus-visible {
698
+ background: var(--bs-alice-blue, #eff5fb);
699
+ color: inherit;
700
+ box-shadow: none;
701
+ }
702
+ .explore-menu-portal .explore-option.active {
703
+ background: var(--bs-alice-blue, #eff5fb);
704
+ color: var(--bs-primary, #0d6efd);
705
+ font-weight: 700;
706
+ box-shadow: none;
707
+ }
708
+ .explore-menu-portal .explore-option .thumb {
709
+ display: inline-flex;
710
+ width: 1.25rem;
711
+ height: 1.25rem;
712
+ align-items: center;
713
+ justify-content: center;
714
+ }
715
+ .explore-menu-portal .explore-option .thumb img {
716
+ width: 1.25rem;
717
+ height: 1.25rem;
718
+ object-fit: cover;
719
+ border-radius: .375rem;
720
+ }
721
+ /* Locked (navigating) state */
722
+ .explore-menu-portal.locked {
723
+ pointer-events: none;
724
+ user-select: none;
725
+ cursor: progress;
726
+ }
727
+ .explore-menu-portal.locked .explore-option {
728
+ opacity: .7;
729
+ }
730
+ .explore-menu-portal.locked .explore-option .spinner-border {
731
+ opacity: 1;
732
+ }
733
+ `}</style>
734
+ </>
735
+ )
736
+ }
737
+
738
+ // Choose which Explore to render (unless renderMainNav override is provided)
739
+ const defaultMainNav = resolvedMainNavMode === 'list'
740
+ ? renderExploreList()
741
+ : renderExploreSelect()
742
+ const mainNavContent = isSettingsRoute
743
+ ? null
744
+ : renderMainNav
745
+ ? renderMainNav(isCollapsed, setIsCollapsed)
746
+ : defaultMainNav
747
+ const shouldShowMainNav = !!mainNavContent
748
+
749
+ return (
750
+ <div
751
+ className="d-flex w-100"
752
+ style={{
753
+ height: isAuthenticated ? '100vh' : 'auto',
754
+ minHeight: isAuthenticated ? '100vh' : 'auto',
755
+ }}
756
+ >
757
+ {isAuthenticated && (
758
+ <>
759
+ {/* ref for width/left */}
760
+ <div
761
+ ref={sidebarRef}
762
+ className={sidebarClass}
763
+ style={{
764
+ width: isCollapsed ? '40px' : '250px',
765
+ zIndex: 1400,
766
+ boxShadow: isMobileOpen ? '2px 0 5px rgba(0,0,0,0.2)' : undefined
767
+ }}
768
+ >
769
+ <div className={`logo-row ${isCollapsed ? 'logo-row-collapsed' : ''}`}>
770
+ <Link
771
+ href="/"
772
+ className="logo-link"
773
+ onClick={handleLogoClick}
774
+ >
775
+ <img src="/images/logo/Osirus-Logo-Icon.svg" alt="Osirus Logo" />
776
+ </Link>
777
+
778
+ {/* Collapse on desktop */}
779
+ {!isCollapsed && !isMobile && (
780
+ <Button
781
+ onClick={() => setIsCollapsed(true)}
782
+ className="collapse-button w-xs-30p h-xs-30p"
783
+ title="Collapse Sidebar"
784
+ ariaLabel="Collapse Sidebar"
785
+ >
786
+ <i className="bi bi-layout-sidebar" />
787
+ </Button>
788
+ )}
789
+
790
+ {/* Expand on desktop */}
791
+ {isCollapsed && !isMobile && (
792
+ <Button
793
+ onClick={() => setIsCollapsed(false)}
794
+ className="expand-button"
795
+ title="Expand Sidebar"
796
+ ariaLabel="Expand Sidebar"
797
+ >
798
+ <i className="bi bi-layout-sidebar-reverse" />
799
+ </Button>
800
+ )}
801
+
802
+ {/* Close (X) on mobile */}
803
+ {isMobile && (
804
+ <Button
805
+ onClick={() => setIsMobileOpen(false)}
806
+ className="position-absolute"
807
+ style={{ top: '8px', right: '10px', width: '18px', height: '18px', minWidth: 'unset', zIndex: 3 }}
808
+ title="Close Sidebar"
809
+ ariaLabel="Close Sidebar"
810
+ >
811
+ <i className="bi bi-x-lg" />
812
+ </Button>
813
+ )}
814
+ </div>
815
+
816
+ {!isCollapsed && orgMemberships.length > 0 && !shouldHideOrgPicker && (
817
+ <div className="mt-4 position-relative">
818
+ {/* Custom Org Picker Button */}
819
+ <button
820
+ ref={orgPickerRef}
821
+ type="button"
822
+ className="w-100 d-flex align-items-center gap-2 px-1 py-2 bg-white border rounded sidebar-nav-trigger"
823
+ style={{
824
+ cursor: isSettingOrg ? 'wait' : 'pointer',
825
+ opacity: isSettingOrg ? 0.7 : 1
826
+ }}
827
+ onClick={() => !isSettingOrg && setIsOrgPickerOpen(v => !v)}
828
+ disabled={isSettingOrg}
829
+ aria-expanded={isOrgPickerOpen}
830
+ aria-haspopup="listbox"
831
+ >
832
+ <OptimizedImage
833
+ src={getOrgIcon(activeMembership)}
834
+ alt={getOrgDisplayName(activeMembership)}
835
+ width={32}
836
+ height={32}
837
+ className="rounded-circle object-fit-cover"
838
+ />
839
+ <span className="flex-grow-1 text-start text-truncate">
840
+ {getOrgDisplayName(activeMembership)}
841
+ </span>
842
+ <i className={`bi bi-chevron-${isOrgPickerOpen ? 'up' : 'down'}`} />
843
+ </button>
844
+
845
+ {/* Org Picker Dropdown (Portal) */}
846
+ {isOrgPickerOpen && createPortal(
847
+ <div
848
+ ref={orgMenuRef}
849
+ role="listbox"
850
+ className="shadow-sm rounded border bg-white"
851
+ style={{
852
+ position: 'fixed',
853
+ zIndex: 3000,
854
+ left: orgPickerRef.current?.getBoundingClientRect().left || 0,
855
+ top: (orgPickerRef.current?.getBoundingClientRect().bottom || 0) + 4,
856
+ width: orgPickerRef.current?.getBoundingClientRect().width || 200,
857
+ maxHeight: 300,
858
+ overflowY: 'auto',
859
+ padding: '4px'
860
+ }}
861
+ >
862
+ {(() => {
863
+ const personalOrgs = orgMemberships.filter(
864
+ (m) => isPersonalMembership(m)
865
+ )
866
+ const businessOrgs = orgMemberships.filter(
867
+ (m) => !isPersonalMembership(m)
868
+ )
869
+
870
+ const renderOrgButton = (membership: typeof orgMemberships[0]) => {
871
+ const isActive = membership.orgId === activeOrgId
872
+ const displayName = getOrgDisplayName(membership)
873
+ return (
874
+ <button
875
+ key={membership.id}
876
+ type="button"
877
+ role="option"
878
+ className={`w-100 text-start d-flex align-items-center gap-2 px-1 py-2 bg-transparent border-0 rounded org-option ${isActive ? 'active' : ''}`}
879
+ onClick={() => handleOrgChange(membership.orgId)}
880
+ aria-selected={isActive}
881
+ >
882
+ <OptimizedImage
883
+ src={getOrgIcon(membership)}
884
+ alt={displayName}
885
+ width={24}
886
+ height={24}
887
+ className="rounded-circle object-fit-cover"
888
+ />
889
+ <span className="flex-grow-1 text-truncate">
890
+ {displayName}
891
+ </span>
892
+ {isActive && <i className="bi bi-check2" />}
893
+ </button>
894
+ )
895
+ }
896
+
897
+ return (
898
+ <>
899
+ {personalOrgs.map(renderOrgButton)}
900
+ {personalOrgs.length > 0 && businessOrgs.length > 0 && (
901
+ <div className="border-top my-1">
902
+ <small className="d-block px-1 pt-2 pb-1 text-muted" style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Organizations</small>
903
+ </div>
904
+ )}
905
+ {businessOrgs.map(renderOrgButton)}
906
+ </>
907
+ )
908
+ })()}
909
+
910
+ {/* Add Organization Button */}
911
+ <div className="border-top mt-1 pt-1">
912
+ <a
913
+ className="w-100 text-start d-flex align-items-center gap-2 px-1 py-2 bg-transparent border-0 rounded org-option add-org-btn text-decoration-none"
914
+ href="/settings/organizations/new"
915
+ onClick={() => setIsOrgPickerOpen(false)}
916
+ >
917
+ <div className="d-flex align-items-center justify-content-center rounded-circle bg-light" style={{ width: 24, height: 24 }}>
918
+ <i className="bi bi-plus" style={{ fontSize: '1rem' }} />
919
+ </div>
920
+ <span className="flex-grow-1 fw-semibold">Add Organization</span>
921
+ </a>
922
+ </div>
923
+ </div>,
924
+ document.body
925
+ )}
926
+
927
+ <style jsx global>{`
928
+ .org-option {
929
+ transition: background-color .12s ease;
930
+ }
931
+ .org-option:hover,
932
+ .org-option:focus-visible {
933
+ background: var(--bs-alice-blue, #eff5fb) !important;
934
+ outline: none;
935
+ }
936
+ .org-option.active {
937
+ background: var(--bs-alice-blue, #eff5fb) !important;
938
+ color: var(--bs-dark-900, #111117);
939
+ }
940
+ `}</style>
941
+ </div>
942
+ )}
943
+
944
+ {sidebarTopContent && sidebarTopContent(isCollapsed, setIsCollapsed)}
945
+
946
+ {/* Main nav: custom renderer or resolved default */}
947
+ {mainNavContent}
948
+
949
+ {sidebarContent && sidebarContent(isCollapsed, setIsCollapsed)}
950
+ </div>
951
+
952
+ {/* Mobile expand toggle */}
953
+ {isMobile && !isMobileOpen && (
954
+ <Button
955
+ onClick={() => setIsMobileOpen(true)}
956
+ className="d-block d-md-none position-fixed"
957
+ title="Open Sidebar"
958
+ ariaLabel="Open Sidebar"
959
+ style={{
960
+ top: '1rem',
961
+ left: '1rem',
962
+ height: '18px',
963
+ width: '18px',
964
+ margin: 0,
965
+ padding: 0,
966
+ minWidth: 'unset',
967
+ zIndex: 1100
968
+ }}
969
+ >
970
+ <i className="bi bi-layout-sidebar-reverse" />
971
+ </Button>
972
+ )}
973
+ </>
974
+ )}
975
+
976
+ <div
977
+ className={`flex-grow-1 d-flex flex-column align-items-center`}
978
+ style={{ overflowY: isAuthenticated ? 'auto' : 'visible', width: '100%' }}
979
+ >
980
+ {isBlogPage ? (
981
+ // ✅ Blog pages: no mainContentArea wrapper (no id / no padding classes)
982
+ <div className={`d-flex justify-content-start flex-column w-100 ${isSlugPage || hasMarketplaceHeader
983
+ ? ''
984
+ : !isAuthenticated
985
+ ? isMainBlogPage
986
+ ? 'container px-4'
987
+ : ''
988
+ : isBlogDetailPage
989
+ ? ''
990
+ : 'authenticated-padding'
991
+ }`}>{children}</div>
992
+ ) : (
993
+ // ✅ Everything else: keep mainContentArea behavior
994
+ <div
995
+ {...mainContentAreaIdProps}
996
+ className={`d-flex justify-content-start flex-column w-100 ${isSlugPage || hasMarketplaceHeader
997
+ ? ''
998
+ : !isAuthenticated
999
+ ? useMainContentAreaId
1000
+ ? 'unauthenticated-padding'
1001
+ : ''
1002
+ : useMainContentAreaId
1003
+ ? ''
1004
+ : 'authenticated-padding'
1005
+ }`}
1006
+ >
1007
+ {children}
1008
+ </div>
1009
+ )}
1010
+ </div>
1011
+
1012
+ </div>
1013
+ )
1014
+ }
1015
+
1016
+ export default SideNavLayout