@revenexx/cover 0.1.0

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 (408) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/app/api/account.ts +8 -0
  4. package/app/api/categories.ts +3 -0
  5. package/app/api/checkout.ts +6 -0
  6. package/app/api/images.ts +4 -0
  7. package/app/api/markets.ts +3 -0
  8. package/app/api/product.ts +3 -0
  9. package/app/api/products.ts +4 -0
  10. package/app/api/search.ts +4 -0
  11. package/app/app.config.ts +29 -0
  12. package/app/assets/css/cover.css +25 -0
  13. package/app/components/account/AccountMenu.vue +30 -0
  14. package/app/components/account/AccountShell.vue +18 -0
  15. package/app/components/account/AccountSidebar.vue +90 -0
  16. package/app/components/account/address/AccountAddressCard.vue +287 -0
  17. package/app/components/account/carts/AccountCartsTable.vue +284 -0
  18. package/app/components/account/dashboard/AccountDashboard.vue +351 -0
  19. package/app/components/account/directorder/AccountDirectOrder.vue +512 -0
  20. package/app/components/account/governance/AccountApprovalLimitsTable.vue +221 -0
  21. package/app/components/account/governance/AccountCostCenterDetail.vue +276 -0
  22. package/app/components/account/governance/AccountCostCentersTable.vue +252 -0
  23. package/app/components/account/governance/AccountRequisitionDetail.vue +295 -0
  24. package/app/components/account/governance/AccountRequisitionsTable.vue +255 -0
  25. package/app/components/account/governance/AccountWorkflowDetail.vue +215 -0
  26. package/app/components/account/governance/AccountWorkflowsTable.vue +168 -0
  27. package/app/components/account/governance/GovernanceApprovalLimitModal.vue +183 -0
  28. package/app/components/account/governance/GovernanceCostCenterModal.vue +188 -0
  29. package/app/components/account/governance/GovernanceWorkflowModal.vue +191 -0
  30. package/app/components/account/orderlists/AccountOrderListDetail.vue +349 -0
  31. package/app/components/account/orderlists/AccountOrderListsTable.vue +352 -0
  32. package/app/components/account/orders/AccountOrderDetail.vue +376 -0
  33. package/app/components/account/orders/AccountOrdersTable.vue +281 -0
  34. package/app/components/account/preferences/AccountPreferences.vue +50 -0
  35. package/app/components/auth/AuthForgotPasswordPanel.vue +88 -0
  36. package/app/components/auth/AuthLoginPanel.vue +103 -0
  37. package/app/components/auth/AuthLoginTeaser.vue +24 -0
  38. package/app/components/auth/AuthPageHeader.vue +21 -0
  39. package/app/components/auth/AuthRegisterPanel.vue +431 -0
  40. package/app/components/auth/AuthRegisterTeaser.vue +24 -0
  41. package/app/components/auth/AuthResetPasswordPanel.vue +115 -0
  42. package/app/components/cart/CartButton.vue +42 -0
  43. package/app/components/cart/actions/CartCostCenterModal.vue +110 -0
  44. package/app/components/cart/actions/CartExportModal.vue +82 -0
  45. package/app/components/cart/actions/CartPositionTextsModal.vue +92 -0
  46. package/app/components/cart/actions/CartSaveToListModal.vue +177 -0
  47. package/app/components/cart/actions/CartWorkflowPickerModal.vue +64 -0
  48. package/app/components/cart/drawer/CartDrawer.vue +49 -0
  49. package/app/components/cart/item/CartItem.vue +318 -0
  50. package/app/components/cart/manage/CartNameModal.vue +68 -0
  51. package/app/components/cart/position/CartPosition.vue +369 -0
  52. package/app/components/cart/quickadd/CartQuickAdd.vue +145 -0
  53. package/app/components/cart/recommend/CartCrossSell.vue +102 -0
  54. package/app/components/cart/recommend/CartRecommendCard.vue +59 -0
  55. package/app/components/cart/recommend/CartReorderStrip.vue +108 -0
  56. package/app/components/cart/requisition/CartRequisitionGroup.vue +74 -0
  57. package/app/components/cart/skeleton/CartSkeleton.vue +18 -0
  58. package/app/components/cart/summary/CartSummary.vue +25 -0
  59. package/app/components/cart/summary/CartSummaryBreakdown.vue +35 -0
  60. package/app/components/cart/switcher/CartSwitcher.vue +147 -0
  61. package/app/components/cart/target/CartTargetModal.vue +129 -0
  62. package/app/components/cart/view/CartApprovalsPanel.vue +89 -0
  63. package/app/components/cart/view/CartListHeader.vue +86 -0
  64. package/app/components/cart/view/CartPositionList.vue +94 -0
  65. package/app/components/cart/view/CartSummaryPanel.vue +254 -0
  66. package/app/components/cart/view/CartView.vue +310 -0
  67. package/app/components/category/info/CategoryPrice.vue +47 -0
  68. package/app/components/category/info/CategorySku.vue +15 -0
  69. package/app/components/category/info/CategoryStock.vue +25 -0
  70. package/app/components/category/list/CategoryItemCount.vue +27 -0
  71. package/app/components/category/list/CategoryListPagination.vue +81 -0
  72. package/app/components/category/skeleton/CategoryDetailSkeleton.vue +12 -0
  73. package/app/components/category/skeleton/CategoryListSkeleton.vue +65 -0
  74. package/app/components/category/view/CategoryViewToggle.vue +33 -0
  75. package/app/components/category/view/GridView.vue +78 -0
  76. package/app/components/category/view/ListView.vue +80 -0
  77. package/app/components/checkout/confirmation/CheckoutConfirmationView.vue +289 -0
  78. package/app/components/checkout/form/CheckoutSchemaForm.vue +72 -0
  79. package/app/components/checkout/onepage/CheckoutAddressPickerModal.vue +260 -0
  80. package/app/components/checkout/onepage/CheckoutAddressSection.vue +186 -0
  81. package/app/components/checkout/onepage/CheckoutDeliverySection.vue +158 -0
  82. package/app/components/checkout/onepage/CheckoutGate.vue +67 -0
  83. package/app/components/checkout/onepage/CheckoutGuestDetailsSection.vue +154 -0
  84. package/app/components/checkout/onepage/CheckoutNotesSection.vue +62 -0
  85. package/app/components/checkout/onepage/CheckoutOnePageView.vue +77 -0
  86. package/app/components/checkout/onepage/CheckoutPaymentSection.vue +181 -0
  87. package/app/components/checkout/onepage/CheckoutSectionTitle.vue +18 -0
  88. package/app/components/checkout/onepage/CheckoutSummarySection.vue +211 -0
  89. package/app/components/checkout/onepage/CheckoutWorkflowPanel.vue +38 -0
  90. package/app/components/layout/AppLogo.vue +21 -0
  91. package/app/components/layout/footer/Footer.vue +148 -0
  92. package/app/components/layout/header/AppBar.vue +75 -0
  93. package/app/components/layout/header/CategoryNav.vue +93 -0
  94. package/app/components/layout/header/HeaderActionItem.vue +81 -0
  95. package/app/components/layout/header/LocaleSwitcher.vue +123 -0
  96. package/app/components/layout/header/MainNav.vue +61 -0
  97. package/app/components/layout/header/MobileMenu.vue +81 -0
  98. package/app/components/layout/header/TopBar.vue +63 -0
  99. package/app/components/listing/ListingView.vue +227 -0
  100. package/app/components/product/detail/ProductDetailAddToCart.vue +188 -0
  101. package/app/components/product/detail/ProductDetailDescription.vue +20 -0
  102. package/app/components/product/detail/ProductDetailImageGallery.vue +58 -0
  103. package/app/components/product/detail/ProductDetailPricing.vue +108 -0
  104. package/app/components/product/detail/ProductDetailSaveToList.vue +291 -0
  105. package/app/components/product/detail/ProductDetailSpecifications.vue +33 -0
  106. package/app/components/product/detail/ProductDetailStock.vue +36 -0
  107. package/app/components/search/filter/SearchFacets.vue +256 -0
  108. package/app/components/search/filter/SearchFacetsDrawer.vue +58 -0
  109. package/app/components/ui/AppShell.vue +7 -0
  110. package/app/components/ui/debug/PiniaStateCard.vue +106 -0
  111. package/app/components/ui/feedback/EmptyState.vue +30 -0
  112. package/app/components/ui/feedback/ErrorBoundary.vue +44 -0
  113. package/app/components/ui/feedback/NotFound.vue +34 -0
  114. package/app/components/ui/forms/CountrySelection.vue +14 -0
  115. package/app/components/ui/forms/FormKitDatePicker.vue +124 -0
  116. package/app/components/ui/forms/PasswordInput.vue +136 -0
  117. package/app/components/ui/forms/SearchInput.vue +213 -0
  118. package/app/components/ui/table/TableSortButton.vue +51 -0
  119. package/app/components/ui/table/TableToolbar.vue +66 -0
  120. package/app/composables/useAccount.ts +27 -0
  121. package/app/composables/useAccountAddresses.ts +116 -0
  122. package/app/composables/useAccountOrders.ts +18 -0
  123. package/app/composables/useAppUrl.ts +38 -0
  124. package/app/composables/useAuthStore.ts +202 -0
  125. package/app/composables/useB2BContext.ts +23 -0
  126. package/app/composables/useCartActions.ts +25 -0
  127. package/app/composables/useCartCalculation.ts +90 -0
  128. package/app/composables/useCartSelection.ts +46 -0
  129. package/app/composables/useCartStore.ts +837 -0
  130. package/app/composables/useCartSummaryFormatting.ts +28 -0
  131. package/app/composables/useCategories.ts +29 -0
  132. package/app/composables/useCheckoutOnePage.ts +438 -0
  133. package/app/composables/useCheckoutProfileSource.ts +8 -0
  134. package/app/composables/useCheckoutSchema.ts +28 -0
  135. package/app/composables/useDataTable.ts +14 -0
  136. package/app/composables/useFormValidation.ts +29 -0
  137. package/app/composables/useIcons.ts +17 -0
  138. package/app/composables/useLocalePreferences.ts +45 -0
  139. package/app/composables/useMarkets.ts +15 -0
  140. package/app/composables/useProduct.ts +89 -0
  141. package/app/composables/useProductCard.ts +8 -0
  142. package/app/composables/useProductCardActions.ts +106 -0
  143. package/app/composables/useProductImage.ts +12 -0
  144. package/app/composables/useProductListing.ts +210 -0
  145. package/app/composables/useProductUrl.ts +17 -0
  146. package/app/composables/useProducts.ts +130 -0
  147. package/app/composables/useSearch.ts +12 -0
  148. package/app/composables/useThemeStore.ts +81 -0
  149. package/app/composables/useViewMode.ts +18 -0
  150. package/app/config/countries.ts +74 -0
  151. package/app/config/icons.ts +283 -0
  152. package/app/config/navigation.ts +191 -0
  153. package/app/config/palette.ts +13 -0
  154. package/app/config/themes.ts +20 -0
  155. package/app/formkit.config.ts +50 -0
  156. package/app/interfaces/account/address-list.ts +34 -0
  157. package/app/interfaces/account/order-list.ts +47 -0
  158. package/app/interfaces/account/order-lists.ts +35 -0
  159. package/app/interfaces/account/profile.ts +17 -0
  160. package/app/interfaces/account.ts +3 -0
  161. package/app/interfaces/address.ts +9 -0
  162. package/app/interfaces/auth.ts +104 -0
  163. package/app/interfaces/b2b.ts +234 -0
  164. package/app/interfaces/cart-calculation.ts +131 -0
  165. package/app/interfaces/cart-item.ts +45 -0
  166. package/app/interfaces/checkout-draft.ts +4 -0
  167. package/app/interfaces/checkout.ts +43 -0
  168. package/app/interfaces/delivery.ts +13 -0
  169. package/app/interfaces/market.ts +24 -0
  170. package/app/interfaces/payment.ts +19 -0
  171. package/app/interfaces/persisted-cart.ts +10 -0
  172. package/app/interfaces/product-detail.ts +54 -0
  173. package/app/interfaces/product-list.ts +33 -0
  174. package/app/interfaces/search-facets.ts +14 -0
  175. package/app/interfaces/validation.ts +14 -0
  176. package/app/layouts/default.vue +78 -0
  177. package/app/layouts/focus.vue +80 -0
  178. package/app/plugins/formkit-locale.client.ts +23 -0
  179. package/app/shared/constants.ts +1 -0
  180. package/app/validations/companyName.ts +18 -0
  181. package/app/validations/emailFormat.ts +10 -0
  182. package/app/validations/formValidationConfig.ts +50 -0
  183. package/app/validations/maxLength.ts +12 -0
  184. package/app/validations/minLength.ts +9 -0
  185. package/app/validations/optionalCompanyName.ts +23 -0
  186. package/app/validations/passwordsMatch.ts +5 -0
  187. package/app/validations/phoneNumber.ts +19 -0
  188. package/app/validations/required.ts +11 -0
  189. package/app/validations/termsRequired.ts +4 -0
  190. package/app/validations/types.ts +10 -0
  191. package/app/validations/zipCode.ts +15 -0
  192. package/i18n/locales/de/account.json +458 -0
  193. package/i18n/locales/de/auth.json +155 -0
  194. package/i18n/locales/de/cart.json +263 -0
  195. package/i18n/locales/de/checkout.json +217 -0
  196. package/i18n/locales/de/common.json +80 -0
  197. package/i18n/locales/de/cover.json +33 -0
  198. package/i18n/locales/de/order.json +1 -0
  199. package/i18n/locales/de/product.json +71 -0
  200. package/i18n/locales/de/search.json +54 -0
  201. package/i18n/locales/de/validation.json +12 -0
  202. package/i18n/locales/en/account.json +458 -0
  203. package/i18n/locales/en/auth.json +155 -0
  204. package/i18n/locales/en/cart.json +263 -0
  205. package/i18n/locales/en/checkout.json +217 -0
  206. package/i18n/locales/en/common.json +80 -0
  207. package/i18n/locales/en/cover.json +33 -0
  208. package/i18n/locales/en/order.json +1 -0
  209. package/i18n/locales/en/product.json +71 -0
  210. package/i18n/locales/en/search.json +54 -0
  211. package/i18n/locales/en/validation.json +12 -0
  212. package/nuxt.config.ts +109 -0
  213. package/package.json +65 -0
  214. package/public/img/product-placeholder.svg +8 -0
  215. package/public/templates/direct-order-template.csv +3 -0
  216. package/public/templates/direct-order-template.xlsx +0 -0
  217. package/server/api/account/address/[id].delete.ts +51 -0
  218. package/server/api/account/address/[id].put.ts +83 -0
  219. package/server/api/account/address/index.post.ts +60 -0
  220. package/server/api/account/addresses.get.ts +25 -0
  221. package/server/api/account/context.get.ts +16 -0
  222. package/server/api/account/governance/[domain]/[id].put.ts +57 -0
  223. package/server/api/account/governance/[domain].post.ts +111 -0
  224. package/server/api/account/order-lists/[id].delete.ts +22 -0
  225. package/server/api/account/order-lists/[id].get.ts +17 -0
  226. package/server/api/account/order-lists/[id].put.ts +46 -0
  227. package/server/api/account/order-lists/index.get.ts +14 -0
  228. package/server/api/account/order-lists/index.post.ts +38 -0
  229. package/server/api/account/orders/[id].get.ts +35 -0
  230. package/server/api/account/orders.get.ts +35 -0
  231. package/server/api/account/profile.get.ts +56 -0
  232. package/server/api/account/profile.put.ts +128 -0
  233. package/server/api/account/requisitions/[id].get.ts +15 -0
  234. package/server/api/account/requisitions.get.ts +23 -0
  235. package/server/api/auth/login.post.ts +37 -0
  236. package/server/api/auth/logout.post.ts +4 -0
  237. package/server/api/auth/me.get.ts +3 -0
  238. package/server/api/auth/personas.get.ts +7 -0
  239. package/server/api/auth/recovery.post.ts +32 -0
  240. package/server/api/auth/recovery.put.ts +37 -0
  241. package/server/api/auth/register.post.ts +126 -0
  242. package/server/api/cart/calculate.post.ts +25 -0
  243. package/server/api/cart/export.post.ts +81 -0
  244. package/server/api/cart/index.delete.ts +10 -0
  245. package/server/api/cart/index.get.ts +10 -0
  246. package/server/api/cart/sync.post.ts +14 -0
  247. package/server/api/carts/[id]/activate.post.ts +18 -0
  248. package/server/api/carts/[id]/index.delete.ts +18 -0
  249. package/server/api/carts/[id]/index.put.ts +19 -0
  250. package/server/api/carts/[id]/items.post.ts +21 -0
  251. package/server/api/carts/index.get.ts +14 -0
  252. package/server/api/carts/index.post.ts +14 -0
  253. package/server/api/categories/[slug].get.ts +22 -0
  254. package/server/api/categories/index.get.ts +11 -0
  255. package/server/api/checkout/profile.get.ts +21 -0
  256. package/server/api/checkout/schema/[key].get.ts +19 -0
  257. package/server/api/checkout/session.post.ts +8 -0
  258. package/server/api/images/detail/[filename].get.ts +18 -0
  259. package/server/api/images/list/[filename].get.ts +17 -0
  260. package/server/api/markets.get.ts +9 -0
  261. package/server/api/orders/index.post.ts +376 -0
  262. package/server/api/payment/methods.post.ts +65 -0
  263. package/server/api/product/[id].get.ts +29 -0
  264. package/server/api/products/[id].get.ts +30 -0
  265. package/server/api/products/index.get.ts +21 -0
  266. package/server/api/requisitions/index.post.ts +67 -0
  267. package/server/api/shipping/rates.post.ts +70 -0
  268. package/server/api/themes/index.get.ts +9 -0
  269. package/server/api/typesense/drop.get.ts +22 -0
  270. package/server/api/typesense/health.get.ts +9 -0
  271. package/server/api/typesense/search.post.ts +199 -0
  272. package/server/api/typesense/seed.get.ts +112 -0
  273. package/server/api/typesense/suggest.post.ts +69 -0
  274. package/server/config/account/organization.json +146 -0
  275. package/server/config/account/personas.json +169 -0
  276. package/server/config/account/user.json +8 -0
  277. package/server/config/account/workflows.json +19 -0
  278. package/server/data/account/address-list.json +103 -0
  279. package/server/data/account/order-list.json +491 -0
  280. package/server/data/account/order-lists.json +149 -0
  281. package/server/data/account/profile.json +9 -0
  282. package/server/data/account/registration-requests.json +3 -0
  283. package/server/data/account/requisitions.json +686 -0
  284. package/server/data/categories.json +186 -0
  285. package/server/data/forms/checkout/add-address.json +24 -0
  286. package/server/data/list/5137-1.png +0 -0
  287. package/server/data/list/5498-1.png +0 -0
  288. package/server/data/list/5498-2.png +0 -0
  289. package/server/data/list/5498-3.png +0 -0
  290. package/server/data/list/5498-4.png +0 -0
  291. package/server/data/list/5498-5.png +0 -0
  292. package/server/data/list/5498-6.png +0 -0
  293. package/server/data/list/5519-1.png +0 -0
  294. package/server/data/list/5713-1.png +0 -0
  295. package/server/data/list/5789-1.png +0 -0
  296. package/server/data/list/5930-1.png +0 -0
  297. package/server/data/list/6127-1.png +0 -0
  298. package/server/data/list/6234-1.png +0 -0
  299. package/server/data/list/6238-1.png +0 -0
  300. package/server/data/list/6246-1.png +0 -0
  301. package/server/data/list/6270-1.png +0 -0
  302. package/server/data/list/6330-1.png +0 -0
  303. package/server/data/list/6336-1.png +0 -0
  304. package/server/data/list/6360-1.png +0 -0
  305. package/server/data/list/6363-1.png +0 -0
  306. package/server/data/list/6375-1.png +0 -0
  307. package/server/data/list/6385-1.png +0 -0
  308. package/server/data/list/6413-1.png +0 -0
  309. package/server/data/list/6418-1.png +0 -0
  310. package/server/data/list/6465-1.png +0 -0
  311. package/server/data/list/6477-1.png +0 -0
  312. package/server/data/list/6509-1.png +0 -0
  313. package/server/data/list/6545-1.png +0 -0
  314. package/server/data/list/6548-1.png +0 -0
  315. package/server/data/list/6566-1.png +0 -0
  316. package/server/data/list/6581-1.png +0 -0
  317. package/server/data/list/6609-1.png +0 -0
  318. package/server/data/list/6611-1.png +0 -0
  319. package/server/data/list/6641-1.png +0 -0
  320. package/server/data/list/6659-1.png +0 -0
  321. package/server/data/list/6662-1.png +0 -0
  322. package/server/data/list/6689-1.png +0 -0
  323. package/server/data/list/6698-1.png +0 -0
  324. package/server/data/list/6701-1.png +0 -0
  325. package/server/data/list/6752-1.png +0 -0
  326. package/server/data/list/6755-1.png +0 -0
  327. package/server/data/list/6837-1.png +0 -0
  328. package/server/data/list/6841-1.png +0 -0
  329. package/server/data/list/6844-1.png +0 -0
  330. package/server/data/list/6846-1.png +0 -0
  331. package/server/data/list/6886-1.png +0 -0
  332. package/server/data/list/6895-1.png +0 -0
  333. package/server/data/list/6897-1.png +0 -0
  334. package/server/data/list/6919-1.png +0 -0
  335. package/server/data/list/6977-1.png +0 -0
  336. package/server/data/list/6983-1.png +0 -0
  337. package/server/data/list/6984-1.png +0 -0
  338. package/server/data/list/6985-1.png +0 -0
  339. package/server/data/list/6986-1.png +0 -0
  340. package/server/data/list/6989-1.png +0 -0
  341. package/server/data/list/6995-1.png +0 -0
  342. package/server/data/list/6998-1.png +0 -0
  343. package/server/data/markets.json +24 -0
  344. package/server/data/product-detail.json +2450 -0
  345. package/server/data/product-list.json +2450 -0
  346. package/server/data/themes.json +8 -0
  347. package/server/interfaces/account.ts +20 -0
  348. package/server/interfaces/auth.ts +32 -0
  349. package/server/interfaces/b2bContext.ts +23 -0
  350. package/server/interfaces/cart.ts +46 -0
  351. package/server/interfaces/cartCalculation.ts +17 -0
  352. package/server/interfaces/category.ts +12 -0
  353. package/server/interfaces/checkoutProfile.ts +5 -0
  354. package/server/interfaces/log.ts +3 -0
  355. package/server/interfaces/market.ts +10 -0
  356. package/server/interfaces/product.ts +21 -0
  357. package/server/interfaces/schema.ts +20 -0
  358. package/server/interfaces/theme.ts +10 -0
  359. package/server/services/ApiAuthService.ts +138 -0
  360. package/server/services/ApiCartService.ts +254 -0
  361. package/server/services/ApiCategoryService.ts +25 -0
  362. package/server/services/ApiMarketService.ts +71 -0
  363. package/server/services/ApiProductService.ts +53 -0
  364. package/server/services/LocalFileCategoryService.ts +23 -0
  365. package/server/services/LocalFileCheckoutProfileService.ts +117 -0
  366. package/server/services/LocalFileProductService.ts +128 -0
  367. package/server/services/LocalFileSchemaService.ts +70 -0
  368. package/server/services/LocalFileThemeService.ts +18 -0
  369. package/server/services/LogService.ts +28 -0
  370. package/server/services/MockAccountService.ts +58 -0
  371. package/server/services/MockAuthService.ts +105 -0
  372. package/server/services/MockB2BContextService.ts +149 -0
  373. package/server/services/MockCartCalculationService.ts +395 -0
  374. package/server/services/MockMarketService.ts +18 -0
  375. package/server/services/SdkAccountService.ts +56 -0
  376. package/server/services/SdkAuthService.ts +83 -0
  377. package/server/services/SessionCartService.ts +31 -0
  378. package/server/utils/accountService.ts +30 -0
  379. package/server/utils/authCookie.ts +13 -0
  380. package/server/utils/authService.ts +43 -0
  381. package/server/utils/b2bService.ts +49 -0
  382. package/server/utils/cartService.ts +59 -0
  383. package/server/utils/categoryService.ts +19 -0
  384. package/server/utils/checkoutProfileService.ts +9 -0
  385. package/server/utils/checkoutSession.ts +38 -0
  386. package/server/utils/coverData.ts +84 -0
  387. package/server/utils/governanceStore.ts +38 -0
  388. package/server/utils/i18n.ts +76 -0
  389. package/server/utils/inventoryService.ts +14 -0
  390. package/server/utils/liveCatalog.ts +234 -0
  391. package/server/utils/liveInventories.ts +76 -0
  392. package/server/utils/liveOrders.ts +139 -0
  393. package/server/utils/livePrices.ts +93 -0
  394. package/server/utils/locale.ts +39 -0
  395. package/server/utils/logService.ts +19 -0
  396. package/server/utils/marketService.ts +24 -0
  397. package/server/utils/orderService.ts +14 -0
  398. package/server/utils/paymentService.ts +14 -0
  399. package/server/utils/priceService.ts +14 -0
  400. package/server/utils/productService.ts +28 -0
  401. package/server/utils/productsSchema.ts +30 -0
  402. package/server/utils/revenexxApi.ts +136 -0
  403. package/server/utils/schemaService.ts +25 -0
  404. package/server/utils/serviceMode.ts +70 -0
  405. package/server/utils/shippingService.ts +14 -0
  406. package/server/utils/shopSdk.ts +88 -0
  407. package/server/utils/themeService.ts +16 -0
  408. package/server/utils/typesense.ts +27 -0
@@ -0,0 +1,89 @@
1
+ import { imagesApi } from "../api/images";
2
+ import { productApi } from "../api/product";
3
+ import { getPriceWithOptionalTax } from "../composables/useProducts";
4
+ import type {
5
+ ProductDetail,
6
+ ProductDetailProduct,
7
+ ProductDetailImage,
8
+ ProductDetailStock,
9
+ ProductDetailAttribute,
10
+ ProductDetailCategory,
11
+ } from "../interfaces/product-detail";
12
+
13
+ export const useProductDetail = (id: MaybeRefOrGetter<string>) => {
14
+ const { public: { locale, currency, taxIncludedPrices } } = useRuntimeConfig();
15
+
16
+ const { data: detail, status, error, refresh } = useFetch<ProductDetail | null>(
17
+ () => productApi.detail(toValue(id)),
18
+ {
19
+ default: () => null,
20
+ },
21
+ );
22
+
23
+ const productInfo = computed<ProductDetailProduct | undefined>(
24
+ () => detail.value?.product[0],
25
+ );
26
+
27
+ const mainImages = computed<ProductDetailImage[]>(() =>
28
+ (detail.value?.images ?? [])
29
+ .filter(img => img.type === "main")
30
+ .sort((a, b) => a.priority - b.priority),
31
+ );
32
+
33
+ const longDescription = computed<string | undefined>(
34
+ () => detail.value?.descriptions.find(d => d.type === "long")?.content,
35
+ );
36
+
37
+ const stocks = computed<ProductDetailStock[]>(
38
+ () => detail.value?.stocks ?? [],
39
+ );
40
+
41
+ const attributes = computed<ProductDetailAttribute[]>(
42
+ () => detail.value?.attributes ?? [],
43
+ );
44
+
45
+ const prices = computed<{ [key: string]: number }[]>(
46
+ () => detail.value?.prices ?? [],
47
+ );
48
+
49
+ const taxRate = computed<number>(() => detail.value?.tax.rate ?? 0);
50
+
51
+ const productCategories = computed<ProductDetailCategory[]>(
52
+ () => detail.value?.categories ?? [],
53
+ );
54
+
55
+ const categorySlug = computed<string | undefined>(() => {
56
+ const slugCat = productCategories.value.find(c => c.type === "slug");
57
+ if (!slugCat) {
58
+ return undefined;
59
+ }
60
+ return slugCat.name.split("/")[0];
61
+ });
62
+
63
+ const subcategorySlug = computed<string | undefined>(() => {
64
+ const slugCat = productCategories.value.find(c => c.type === "slug");
65
+ if (!slugCat) {
66
+ return undefined;
67
+ }
68
+ return slugCat.name.split("/")[1] !== "product" ? slugCat.name.split("/")[1] : undefined;
69
+ });
70
+
71
+ const formatPrice = (cents: number): string => {
72
+ const basePrice = cents / 100;
73
+ const displayPrice = getPriceWithOptionalTax(basePrice, taxRate.value, taxIncludedPrices);
74
+
75
+ return displayPrice.toLocaleString(locale, { style: "currency", currency });
76
+ };
77
+
78
+ const getDetailImageUrl = (filename: string): string => {
79
+ return imagesApi.detail(filename);
80
+ };
81
+
82
+ return {
83
+ detail, status, error, refresh,
84
+ productInfo, mainImages, longDescription,
85
+ stocks, attributes, prices, taxRate,
86
+ productCategories, categorySlug, subcategorySlug,
87
+ formatPrice, getDetailImageUrl,
88
+ };
89
+ };
@@ -0,0 +1,8 @@
1
+ import type { ProductList } from "../interfaces/product-list";
2
+
3
+ export function useProductCard(getProduct: () => ProductList) {
4
+ const url = computed(() => useProductUrl(getProduct()).value);
5
+ const { getImage } = useProductImage();
6
+
7
+ return { url, getImage };
8
+ }
@@ -0,0 +1,106 @@
1
+ import { getBaseUnitPrice } from "../composables/useProducts";
2
+ import type { CartItem, CartItemPriceTier } from "../interfaces/cart-item";
3
+ import type { ProductList } from "../interfaces/product-list";
4
+
5
+ export function useProductCardActions(getProduct: () => ProductList) {
6
+ const cart = useCartStore();
7
+ const toast = useToast();
8
+ const { icon } = useIcons();
9
+ const { t } = useI18n();
10
+
11
+ const isOutOfStock = computed(() => {
12
+ const product = getProduct();
13
+ if (typeof product.isOutOfStock === "boolean") {
14
+ return product.isOutOfStock;
15
+ }
16
+ return (product.stocks?.[0]?.quantity ?? 0) <= 0;
17
+ });
18
+
19
+ /** Price on request (no price for this buyer context) — not orderable online. */
20
+ const hasPrice = computed(() => getBaseUnitPrice(getProduct().prices ?? []) !== null);
21
+
22
+ function buildPriceTiers(prices: { [key: string]: number }[]): CartItemPriceTier[] {
23
+ if (!prices.length) {
24
+ return [];
25
+ }
26
+
27
+ const priceMap = prices[0];
28
+ if (!priceMap) {
29
+ return [];
30
+ }
31
+
32
+ return Object.entries(priceMap)
33
+ .map(([qty, totalCents]) => {
34
+ const minQuantity = parseInt(qty, 10);
35
+ return {
36
+ minQuantity,
37
+ unitPrice: Number(totalCents) / minQuantity / 100,
38
+ };
39
+ })
40
+ .sort((a, b) => a.minQuantity - b.minQuantity);
41
+ }
42
+
43
+ function addToCart() {
44
+ const product = getProduct();
45
+
46
+ if (isOutOfStock.value || !hasPrice.value) {
47
+ return;
48
+ }
49
+
50
+ const quantity = 1;
51
+ const maxQuantity = product.maxOrderQuantity;
52
+ const currentCartQuantity
53
+ = cart.items.find((item: CartItem) => item.id === product.id)?.quantity ?? 0;
54
+
55
+ if (typeof maxQuantity === "number" && currentCartQuantity + quantity > maxQuantity) {
56
+ toast.add({
57
+ title: t("product.actions.maxQuantityReachedTitle", { count: currentCartQuantity }),
58
+ description: t("product.actions.maxQuantityReachedDescription", { max: maxQuantity }),
59
+ color: "error",
60
+ icon: icon("alert"),
61
+ duration: 3000,
62
+ });
63
+ return;
64
+ }
65
+
66
+ const baseUnitPrice = getBaseUnitPrice(product.prices);
67
+ if (baseUnitPrice === null) {
68
+ return;
69
+ }
70
+
71
+ const added = cart.addItem({
72
+ id: product.id,
73
+ name: product.name,
74
+ image: product.image,
75
+ sku: product.sku,
76
+ price: baseUnitPrice,
77
+ taxRate: product.tax.rate,
78
+ priceTiers: buildPriceTiers(product.prices),
79
+ categorySlug: product.categorySlug,
80
+ subcategorySlug: product.subcategorySlug,
81
+ ...(typeof maxQuantity === "number" ? { maxOrderQuantity: maxQuantity } : {}),
82
+ }, { chooseTarget: true });
83
+
84
+ if (!added) {
85
+ if (typeof maxQuantity === "number") {
86
+ toast.add({
87
+ title: t("product.actions.maxQuantityReachedTitle", { count: currentCartQuantity }),
88
+ description: t("product.actions.maxQuantityReachedDescription", { max: maxQuantity }),
89
+ color: "error",
90
+ icon: icon("alert"),
91
+ duration: 3000,
92
+ });
93
+ }
94
+ return;
95
+ }
96
+
97
+ toast.add({
98
+ title: t("product.actions.addedToCart", { quantity, name: product.name }),
99
+ color: "primary",
100
+ icon: icon("check"),
101
+ duration: 1000,
102
+ });
103
+ }
104
+
105
+ return { isOutOfStock, hasPrice, addToCart };
106
+ }
@@ -0,0 +1,12 @@
1
+ import { imagesApi } from "../api/images";
2
+
3
+ export function useProductImage() {
4
+ const getImage = (name: string): string => {
5
+ if (name.startsWith("/") || name.startsWith("http")) {
6
+ return name;
7
+ }
8
+ return imagesApi.list(name);
9
+ };
10
+
11
+ return { getImage };
12
+ }
@@ -0,0 +1,210 @@
1
+ import { searchApi } from "../api/search";
2
+ import type { ProductList } from "../interfaces/product-list";
3
+ import type { PriceRange, SearchFacet } from "../interfaces/search-facets";
4
+
5
+ export interface ProductListingOptions {
6
+ /** Unique fetch key per listing surface (e.g. "search", "category"). */
7
+ key: string;
8
+ /** Reactive full-text query; defaults to a match-all listing. */
9
+ query?: () => string;
10
+ /**
11
+ * Filters applied on top of the user's selection but never shown as
12
+ * chips or facets — e.g. the category of a category page.
13
+ */
14
+ fixedFilters?: () => Record<string, string[]>;
15
+ /** Facet fields offered to the user, in display order. */
16
+ facetFields?: string[];
17
+ /** Run the first request even with an empty query (category pages). */
18
+ allowEmptyQuery?: boolean;
19
+ }
20
+
21
+ const DEFAULT_FACETS = ["category", "subcategory", "manufacturer", "available"];
22
+
23
+ export interface ListingResponse {
24
+ products: ProductList[];
25
+ facets: SearchFacet[];
26
+ found: number;
27
+ page: number;
28
+ perPage: number;
29
+ }
30
+
31
+ /**
32
+ * The shared product listing engine: one Typesense-backed state machine
33
+ * for the search page and the category pages — full-text query, facet
34
+ * filters, price range, sorting and pagination, all synced to the URL.
35
+ */
36
+ export function useProductListing(options: ProductListingOptions) {
37
+ const route = useRoute();
38
+ const router = useRouter();
39
+ const { t } = useI18n();
40
+ const { productsPerPage } = useRuntimeConfig().public;
41
+
42
+ const facetFields = options.facetFields ?? DEFAULT_FACETS;
43
+ const query = computed(() => options.query?.() ?? "");
44
+
45
+ /* ---- URL state ---------------------------------------------------- */
46
+ function updateUrl(updates: Record<string, string | number | null | undefined>) {
47
+ const merged: Record<string, string | null> = {};
48
+ for (const [k, v] of Object.entries(route.query)) {
49
+ if (typeof v === "string") {
50
+ merged[k] = v;
51
+ }
52
+ }
53
+ for (const [key, value] of Object.entries(updates)) {
54
+ merged[key] = (value !== null && value !== undefined) ? String(value) : null;
55
+ }
56
+ if (!("page" in updates)) {
57
+ merged.page = null;
58
+ }
59
+ const next: Record<string, string> = {};
60
+ for (const [k, v] of Object.entries(merged)) {
61
+ if (v !== null) {
62
+ next[k] = v;
63
+ }
64
+ }
65
+ void router.replace({ query: next });
66
+ }
67
+
68
+ const sort = computed({
69
+ get: () => (route.query.sort as string) ?? "relevance",
70
+ set: (value: string) => updateUrl({ sort: value === "relevance" ? null : value }),
71
+ });
72
+
73
+ const page = computed({
74
+ get: () => Math.max(1, parseInt((route.query.page as string) ?? "1", 10)),
75
+ set: (value: number) => updateUrl({ page: value === 1 ? null : value }),
76
+ });
77
+
78
+ const activeFilters = computed<Record<string, string[]>>({
79
+ get() {
80
+ const result: Record<string, string[]> = {};
81
+ for (const field of facetFields) {
82
+ const raw = route.query[field] as string | undefined;
83
+ if (raw) {
84
+ result[field] = raw.split(",").filter(Boolean);
85
+ }
86
+ }
87
+ return result;
88
+ },
89
+ set(filters: Record<string, string[]>) {
90
+ const updates: Record<string, string | null> = {};
91
+ for (const field of facetFields) {
92
+ const values = filters[field];
93
+ updates[field] = values?.length ? values.join(",") : null;
94
+ }
95
+ updateUrl(updates);
96
+ },
97
+ });
98
+
99
+ const priceRange = computed<PriceRange>({
100
+ get: () => ({
101
+ min: route.query.price_min ? parseInt(route.query.price_min as string, 10) : undefined,
102
+ max: route.query.price_max ? parseInt(route.query.price_max as string, 10) : undefined,
103
+ }),
104
+ set(value: PriceRange) {
105
+ updateUrl({ price_min: value.min ?? null, price_max: value.max ?? null });
106
+ },
107
+ });
108
+
109
+ /* ---- fetching ------------------------------------------------------ */
110
+ const hasQuery = computed(() =>
111
+ options.allowEmptyQuery ? true : query.value.trim().length > 0,
112
+ );
113
+
114
+ const requestBody = computed(() => ({
115
+ q: query.value.trim() || "*",
116
+ filters: { ...activeFilters.value, ...(options.fixedFilters?.() ?? {}) },
117
+ priceRange: priceRange.value,
118
+ sort: sort.value,
119
+ page: page.value,
120
+ perPage: productsPerPage as number,
121
+ }));
122
+
123
+ const { data, pending, error, execute } = useFetch<ListingResponse | null>(
124
+ searchApi.search(),
125
+ {
126
+ key: `listing:${options.key}`,
127
+ method: "POST",
128
+ body: requestBody,
129
+ immediate: false,
130
+ watch: false,
131
+ },
132
+ );
133
+
134
+ if (hasQuery.value) {
135
+ // First load (SSR-capable), then re-fetch on every URL state change.
136
+ void execute();
137
+ }
138
+ watch(
139
+ () => [route.query, query.value],
140
+ () => {
141
+ if (hasQuery.value) {
142
+ void execute();
143
+ }
144
+ },
145
+ { deep: true },
146
+ );
147
+
148
+ /* ---- derived view state -------------------------------------------- */
149
+ const products = computed<ProductList[]>(() => data.value?.products ?? []);
150
+ const found = computed(() => data.value?.found ?? 0);
151
+ const totalPages = computed(() => Math.ceil(found.value / (productsPerPage as number)));
152
+ const isEmpty = computed(() => data.value !== null && data.value !== undefined && products.value.length === 0);
153
+
154
+ /** Facets in configured order, slug facets hidden from the UI. */
155
+ const facets = computed<SearchFacet[]>(() => {
156
+ const byField = new Map((data.value?.facets ?? []).map(f => [f.fieldName, f]));
157
+ return facetFields
158
+ .map(field => byField.get(field))
159
+ .filter((f): f is SearchFacet => Boolean(f && f.counts.length > 0));
160
+ });
161
+
162
+ const activeFilterCount = computed(() =>
163
+ Object.values(activeFilters.value).reduce((sum, values) => sum + values.length, 0)
164
+ + (priceRange.value.min !== undefined || priceRange.value.max !== undefined ? 1 : 0),
165
+ );
166
+
167
+ const chips = computed(() => {
168
+ const list: { field: string; value: string }[] = [];
169
+ for (const [field, values] of Object.entries(activeFilters.value)) {
170
+ for (const value of values) {
171
+ list.push({ field, value });
172
+ }
173
+ }
174
+ return list;
175
+ });
176
+
177
+ function removeChip(field: string, value: string) {
178
+ const current = activeFilters.value[field] ?? [];
179
+ activeFilters.value = {
180
+ ...activeFilters.value,
181
+ [field]: current.filter(v => v !== value),
182
+ };
183
+ }
184
+
185
+ function clearFilters() {
186
+ const updates: Record<string, null> = { price_min: null, price_max: null };
187
+ for (const field of facetFields) {
188
+ updates[field] = null;
189
+ }
190
+ updateUrl(updates);
191
+ }
192
+
193
+ const sortOptions = computed(() => [
194
+ { value: "relevance", label: t("search.sort.relevance") },
195
+ { value: "price_asc", label: t("search.sort.priceAsc") },
196
+ { value: "price_desc", label: t("search.sort.priceDesc") },
197
+ { value: "name_asc", label: t("search.sort.nameAsc") },
198
+ { value: "name_desc", label: t("search.sort.nameDesc") },
199
+ ]);
200
+
201
+ return {
202
+ query, sort, sortOptions, page, activeFilters, priceRange,
203
+ data, pending, error, execute,
204
+ products, facets, found, totalPages, isEmpty,
205
+ chips, activeFilterCount, removeChip, clearFilters,
206
+ perPage: productsPerPage as number,
207
+ };
208
+ }
209
+
210
+ export type ProductListing = ReturnType<typeof useProductListing>;
@@ -0,0 +1,17 @@
1
+ import type { Product } from "../composables/useProducts";
2
+ import { slugify } from "../composables/useProducts";
3
+
4
+ /**
5
+ * Generate a localized product URL with the current locale prefix.
6
+ * Replaces the plain `productUrl()` function to support i18n routing.
7
+ */
8
+ export function useProductUrl(product: Product) {
9
+ const localePath = useLocalePath();
10
+
11
+ const url = computed(() => {
12
+ const path = `/category/${product.categorySlug}/${product.subcategorySlug}/product/${slugify(product)}`;
13
+ return localePath(path);
14
+ });
15
+
16
+ return url;
17
+ }
@@ -0,0 +1,130 @@
1
+ import { productsApi } from "../api/products";
2
+ import type { ProductList, ProductPriceTier } from "../interfaces/product-list";
3
+
4
+ export type Product = ProductList;
5
+ export type { ProductPriceTier };
6
+
7
+ export function getPriceTiers(prices: { [key: string]: number }[]): ProductPriceTier[] {
8
+ const priceMap = prices[0];
9
+ if (!priceMap) {
10
+ return [];
11
+ }
12
+
13
+ return Object.entries(priceMap)
14
+ .map(([quantityRaw, totalCentsRaw]) => {
15
+ const quantity = parseInt(quantityRaw, 10);
16
+ const totalCents = Number(totalCentsRaw);
17
+
18
+ if (!Number.isFinite(quantity) || quantity <= 0) {
19
+ return null;
20
+ }
21
+ if (!Number.isFinite(totalCents)) {
22
+ return null;
23
+ }
24
+
25
+ return {
26
+ quantity,
27
+ totalCents,
28
+ unitCents: totalCents / quantity,
29
+ };
30
+ })
31
+ .filter((tier): tier is ProductPriceTier => tier !== null)
32
+ .sort((a, b) => a.quantity - b.quantity);
33
+ }
34
+
35
+ export function getDisplayUnitPrice(prices: { [key: string]: number }[]): number | null {
36
+ const tiers = getPriceTiers(prices);
37
+ if (!tiers.length) {
38
+ return null;
39
+ }
40
+
41
+ const selectedTier = tiers.length > 1 ? tiers[tiers.length - 1]! : tiers[0]!;
42
+ return selectedTier.unitCents / 100;
43
+ }
44
+
45
+ export function getPriceWithOptionalTax(
46
+ price: number,
47
+ taxRate: number,
48
+ taxIncludedPrices: boolean,
49
+ ): number {
50
+ if (!taxIncludedPrices) {
51
+ return price;
52
+ }
53
+
54
+ const normalizedTaxRate = taxRate > 1 ? taxRate / 100 : taxRate;
55
+ if (!Number.isFinite(normalizedTaxRate) || normalizedTaxRate < 0) {
56
+ return price;
57
+ }
58
+
59
+ return price * (1 + normalizedTaxRate);
60
+ }
61
+
62
+ export function getBaseUnitPrice(prices: { [key: string]: number }[]): number | null {
63
+ const tiers = getPriceTiers(prices);
64
+ if (!tiers.length) {
65
+ return null;
66
+ }
67
+
68
+ return tiers[0]!.unitCents / 100;
69
+ }
70
+
71
+ export function slugify(product: Product): string {
72
+ return `${product.id}-${product.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")}`;
73
+ }
74
+
75
+ const UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
76
+
77
+ /**
78
+ * Extract the product id from a `{id}-{name}` slug. Live ids are UUIDs —
79
+ * which contain dashes themselves — so match the UUID prefix first and only
80
+ * fall back to the first dash-delimited segment for plain (demo) ids.
81
+ */
82
+ export function parseProductIdFromSlug(slug: string): string {
83
+ const uuid = slug.match(UUID_PREFIX);
84
+ if (uuid) {
85
+ return uuid[0];
86
+ }
87
+ return slug.split("-")[0] ?? "";
88
+ }
89
+
90
+ // TODO(i18n-routing): productUrl is a plain function with no composable context, so it cannot
91
+ // call useLocalePath() directly. Fix: convert to a composable (useProductUrl) that calls
92
+ // useLocalePath() and returns a computed URL, or accept localePath as a parameter.
93
+ export function productUrl(product: Product): string {
94
+ return `/category/${product.categorySlug}/product/${slugify(product)}`;
95
+ }
96
+
97
+ export const useProducts = (
98
+ categorySlug?: MaybeRefOrGetter<string | undefined>,
99
+ subcategorySlug?: MaybeRefOrGetter<string | undefined>,
100
+ ) => {
101
+ const { locale } = useI18n();
102
+ const query = computed(() => {
103
+ const q: Record<string, string> = {};
104
+ const category = toValue(categorySlug);
105
+ const subcategory = toValue(subcategorySlug);
106
+ if (category) {
107
+ q.category = category;
108
+ }
109
+ if (subcategory) {
110
+ q.subcategory = subcategory;
111
+ }
112
+ return q;
113
+ });
114
+
115
+ const { data: products, status, error, refresh } = useFetch<Product[]>(productsApi.list(), {
116
+ query,
117
+ headers: computed(() => ({ "x-locale": locale.value })),
118
+ default: () => [] as Product[],
119
+ });
120
+ return { products, status, error, refresh };
121
+ };
122
+
123
+ export const useProduct = (id: string) => {
124
+ const { locale } = useI18n();
125
+ const { data: product, status, error, refresh } = useFetch<Product | null>(productsApi.byId(id), {
126
+ headers: computed(() => ({ "x-locale": locale.value })),
127
+ default: () => null,
128
+ });
129
+ return { product, status, error, refresh };
130
+ };
@@ -0,0 +1,12 @@
1
+ export function useSearch() {
2
+ const localePath = useLocalePath();
3
+
4
+ async function navigateToSearch(q: string): Promise<void> {
5
+ await navigateTo({
6
+ path: localePath("search"),
7
+ query: q.trim() ? { q: q.trim() } : {},
8
+ });
9
+ }
10
+
11
+ return { navigateToSearch };
12
+ }
@@ -0,0 +1,81 @@
1
+ import { defineStore } from "pinia";
2
+
3
+ import { DEFAULT_ICON_WEIGHT, SOLAR_ICON_WEIGHTS, nuxtUiIcons, type SolarIconWeight } from "../config/icons";
4
+ import { palette, COLOR_SHADES } from "../config/palette";
5
+ import { DEFAULT_THEME_ID, themes, type Theme } from "../config/themes";
6
+
7
+ const THEME_STORAGE_KEY = "cover-theme";
8
+ const ICON_WEIGHT_STORAGE_KEY = "cover-icon-weight";
9
+
10
+ export const useThemeStore = defineStore("theme", () => {
11
+ const activeTheme = ref<Theme | null>(null);
12
+
13
+ /** Dev/debug override; `null` follows the active theme's weight. */
14
+ const iconWeightOverride = ref<SolarIconWeight | null>(null);
15
+
16
+ const iconWeight = computed<SolarIconWeight>(
17
+ () => iconWeightOverride.value ?? activeTheme.value?.icons.weight ?? DEFAULT_ICON_WEIGHT,
18
+ );
19
+
20
+ const applyIcons = () => {
21
+ updateAppConfig({ ui: { icons: nuxtUiIcons(iconWeight.value) } });
22
+ };
23
+
24
+ const applyTheme = (theme: Theme) => {
25
+ updateAppConfig({
26
+ ui: { colors: { primary: theme.colors.primary, neutral: theme.colors.neutral } },
27
+ });
28
+ applyIcons();
29
+ if (import.meta.client) {
30
+ const root = document.documentElement;
31
+ const pc = palette[theme.colors.primary];
32
+ const nc = palette[theme.colors.neutral];
33
+ COLOR_SHADES.forEach((s) => {
34
+ root.style.setProperty(`--ui-color-primary-${s}`, pc[s]);
35
+ root.style.setProperty(`--ui-color-neutral-${s}`, nc[s]);
36
+ });
37
+ root.style.setProperty("--ui-radius", theme.tokens.radius);
38
+ }
39
+ };
40
+
41
+ const setTheme = (theme: Theme) => {
42
+ activeTheme.value = theme;
43
+ applyTheme(theme);
44
+ if (import.meta.client) {
45
+ localStorage.setItem(THEME_STORAGE_KEY, theme.id);
46
+ }
47
+ };
48
+
49
+ const setIconWeight = (weight: SolarIconWeight | null) => {
50
+ iconWeightOverride.value = weight;
51
+ applyIcons();
52
+ if (import.meta.client) {
53
+ if (weight) {
54
+ localStorage.setItem(ICON_WEIGHT_STORAGE_KEY, weight);
55
+ }
56
+ else {
57
+ localStorage.removeItem(ICON_WEIGHT_STORAGE_KEY);
58
+ }
59
+ }
60
+ };
61
+
62
+ /** Restores theme + icon weight override from localStorage (client only). */
63
+ const restore = () => {
64
+ if (!import.meta.client) {
65
+ return;
66
+ }
67
+ const savedWeight = localStorage.getItem(ICON_WEIGHT_STORAGE_KEY) as SolarIconWeight | null;
68
+ if (savedWeight && SOLAR_ICON_WEIGHTS.includes(savedWeight)) {
69
+ iconWeightOverride.value = savedWeight;
70
+ }
71
+ const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
72
+ const theme = themes.find((t) => t.id === savedTheme)
73
+ ?? themes.find((t) => t.id === DEFAULT_THEME_ID)
74
+ ?? themes[0];
75
+ if (theme) {
76
+ setTheme(theme);
77
+ }
78
+ };
79
+
80
+ return { activeTheme, themes, setTheme, applyTheme, iconWeight, iconWeightOverride, setIconWeight, restore, DEFAULT_THEME_ID };
81
+ });
@@ -0,0 +1,18 @@
1
+ const ONE_YEAR = 60 * 60 * 24 * 365;
2
+
3
+ export function useViewMode() {
4
+ const viewMode = useCookie<"list" | "grid">("cover-view-mode", {
5
+ default: () => "grid",
6
+ maxAge: ONE_YEAR,
7
+ });
8
+ const sort = useCookie<string>("cover-sort-mode", {
9
+ default: () => "featured",
10
+ maxAge: ONE_YEAR,
11
+ });
12
+ const page = useCookie<number>("cover-page-number", {
13
+ default: () => 1,
14
+ maxAge: ONE_YEAR,
15
+ });
16
+
17
+ return { viewMode, sort, page };
18
+ }