@vendure-platform/create 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 (531) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/demo-data/branding/logo.svg +4 -0
  3. package/dist/assets/demo-data/images/absorbent-pad.webp +0 -0
  4. package/dist/assets/demo-data/images/adjustable-wrench.webp +0 -0
  5. package/dist/assets/demo-data/images/anchor-bolt.webp +0 -0
  6. package/dist/assets/demo-data/images/angle-grinder.webp +0 -0
  7. package/dist/assets/demo-data/images/barrier-tape.webp +0 -0
  8. package/dist/assets/demo-data/images/bench-grinder.webp +0 -0
  9. package/dist/assets/demo-data/images/bracket.webp +0 -0
  10. package/dist/assets/demo-data/images/broom.webp +0 -0
  11. package/dist/assets/demo-data/images/bucket.webp +0 -0
  12. package/dist/assets/demo-data/images/cable-gland.webp +0 -0
  13. package/dist/assets/demo-data/images/cable-tie.webp +0 -0
  14. package/dist/assets/demo-data/images/cable-tray.webp +0 -0
  15. package/dist/assets/demo-data/images/cable.webp +0 -0
  16. package/dist/assets/demo-data/images/chimney-base-kit.webp +0 -0
  17. package/dist/assets/demo-data/images/chimney-cowl-bird-guard.webp +0 -0
  18. package/dist/assets/demo-data/images/chimney-cowl-open.webp +0 -0
  19. package/dist/assets/demo-data/images/chimney-storm-collar.webp +0 -0
  20. package/dist/assets/demo-data/images/chimney-wall-bracket.webp +0 -0
  21. package/dist/assets/demo-data/images/circular-saw.webp +0 -0
  22. package/dist/assets/demo-data/images/cleaning-spray.webp +0 -0
  23. package/dist/assets/demo-data/images/conduit.webp +0 -0
  24. package/dist/assets/demo-data/images/cordless-drill.webp +0 -0
  25. package/dist/assets/demo-data/images/drill-bits.webp +0 -0
  26. package/dist/assets/demo-data/images/dust-mask.webp +0 -0
  27. package/dist/assets/demo-data/images/ear-defenders.webp +0 -0
  28. package/dist/assets/demo-data/images/ear-plugs.webp +0 -0
  29. package/dist/assets/demo-data/images/extension-lead.webp +0 -0
  30. package/dist/assets/demo-data/images/eye-wash-station.webp +0 -0
  31. package/dist/assets/demo-data/images/face-shield.webp +0 -0
  32. package/dist/assets/demo-data/images/file-set.webp +0 -0
  33. package/dist/assets/demo-data/images/first-aid-kit.webp +0 -0
  34. package/dist/assets/demo-data/images/floor-sign.webp +0 -0
  35. package/dist/assets/demo-data/images/flue-bend-45.webp +0 -0
  36. package/dist/assets/demo-data/images/flue-bend-90.webp +0 -0
  37. package/dist/assets/demo-data/images/flue-section-1000mm.webp +0 -0
  38. package/dist/assets/demo-data/images/flue-section-250mm.webp +0 -0
  39. package/dist/assets/demo-data/images/flue-section-500mm.webp +0 -0
  40. package/dist/assets/demo-data/images/fuse.webp +0 -0
  41. package/dist/assets/demo-data/images/glove-dispenser.webp +0 -0
  42. package/dist/assets/demo-data/images/grinding-disc.webp +0 -0
  43. package/dist/assets/demo-data/images/hacksaw.webp +0 -0
  44. package/dist/assets/demo-data/images/hammer.webp +0 -0
  45. package/dist/assets/demo-data/images/hand-sanitiser.webp +0 -0
  46. package/dist/assets/demo-data/images/head-torch.webp +0 -0
  47. package/dist/assets/demo-data/images/heat-gun.webp +0 -0
  48. package/dist/assets/demo-data/images/hex-bolt.webp +0 -0
  49. package/dist/assets/demo-data/images/hex-key-set.webp +0 -0
  50. package/dist/assets/demo-data/images/hex-nut.webp +0 -0
  51. package/dist/assets/demo-data/images/hi-vis-jacket.webp +0 -0
  52. package/dist/assets/demo-data/images/hi-vis-vest.webp +0 -0
  53. package/dist/assets/demo-data/images/hose-clamp.webp +0 -0
  54. package/dist/assets/demo-data/images/impact-driver.webp +0 -0
  55. package/dist/assets/demo-data/images/jigsaw.webp +0 -0
  56. package/dist/assets/demo-data/images/junction-box.webp +0 -0
  57. package/dist/assets/demo-data/images/knee-pads.webp +0 -0
  58. package/dist/assets/demo-data/images/machine-screw.webp +0 -0
  59. package/dist/assets/demo-data/images/made-to-order-configurable-chimney.webp +0 -0
  60. package/dist/assets/demo-data/images/mallet.webp +0 -0
  61. package/dist/assets/demo-data/images/mcb.webp +0 -0
  62. package/dist/assets/demo-data/images/mop.webp +0 -0
  63. package/dist/assets/demo-data/images/paper-towels.webp +0 -0
  64. package/dist/assets/demo-data/images/pipe-wrench.webp +0 -0
  65. package/dist/assets/demo-data/images/pliers-set.webp +0 -0
  66. package/dist/assets/demo-data/images/pry-bar.webp +0 -0
  67. package/dist/assets/demo-data/images/rcd.webp +0 -0
  68. package/dist/assets/demo-data/images/reciprocating-saw.webp +0 -0
  69. package/dist/assets/demo-data/images/rivet.webp +0 -0
  70. package/dist/assets/demo-data/images/roof-flashing-high-pitch.webp +0 -0
  71. package/dist/assets/demo-data/images/roof-flashing-low-pitch.webp +0 -0
  72. package/dist/assets/demo-data/images/safety-boots.webp +0 -0
  73. package/dist/assets/demo-data/images/safety-glasses.webp +0 -0
  74. package/dist/assets/demo-data/images/safety-harness.webp +0 -0
  75. package/dist/assets/demo-data/images/safety-helmet.webp +0 -0
  76. package/dist/assets/demo-data/images/safety-sign.webp +0 -0
  77. package/dist/assets/demo-data/images/sanding-disc.webp +0 -0
  78. package/dist/assets/demo-data/images/saw-blade.webp +0 -0
  79. package/dist/assets/demo-data/images/screwdriver-set.webp +0 -0
  80. package/dist/assets/demo-data/images/screwdriver.webp +0 -0
  81. package/dist/assets/demo-data/images/self-tapping-screw.webp +0 -0
  82. package/dist/assets/demo-data/images/socket-set.webp +0 -0
  83. package/dist/assets/demo-data/images/socket.webp +0 -0
  84. package/dist/assets/demo-data/images/spanner-set.webp +0 -0
  85. package/dist/assets/demo-data/images/spanner.webp +0 -0
  86. package/dist/assets/demo-data/images/spill-kit.webp +0 -0
  87. package/dist/assets/demo-data/images/spirit-level.webp +0 -0
  88. package/dist/assets/demo-data/images/switch.webp +0 -0
  89. package/dist/assets/demo-data/images/tape-measure.webp +0 -0
  90. package/dist/assets/demo-data/images/terminal-block.webp +0 -0
  91. package/dist/assets/demo-data/images/threaded-rod.webp +0 -0
  92. package/dist/assets/demo-data/images/torque-wrench.webp +0 -0
  93. package/dist/assets/demo-data/images/utility-knife.webp +0 -0
  94. package/dist/assets/demo-data/images/wall-pass-sleeve.webp +0 -0
  95. package/dist/assets/demo-data/images/wall-plug.webp +0 -0
  96. package/dist/assets/demo-data/images/washer.webp +0 -0
  97. package/dist/assets/demo-data/images/waste-bin.webp +0 -0
  98. package/dist/assets/demo-data/images/wire-connector.webp +0 -0
  99. package/dist/assets/demo-data/images/wire-cutters.webp +0 -0
  100. package/dist/assets/demo-data/images/wood-screw.webp +0 -0
  101. package/dist/assets/demo-data/images/work-gloves.webp +0 -0
  102. package/dist/assets/demo-data/images/work-light.webp +0 -0
  103. package/dist/assets/demo-data/initial-data.json +667 -0
  104. package/dist/assets/demo-data/products.csv +779 -0
  105. package/dist/assets/storefront/.env.example +14 -0
  106. package/dist/assets/storefront/.env.local +1 -0
  107. package/dist/assets/storefront/components.json +22 -0
  108. package/dist/assets/storefront/eslint.config.mjs +23 -0
  109. package/dist/assets/storefront/graphql.config.yml +2 -0
  110. package/dist/assets/storefront/messages/de.json +1196 -0
  111. package/dist/assets/storefront/messages/en.json +1196 -0
  112. package/dist/assets/storefront/next-env.d.ts +6 -0
  113. package/dist/assets/storefront/next.config.ts +73 -0
  114. package/dist/assets/storefront/package.json +77 -0
  115. package/dist/assets/storefront/postcss.config.mjs +5 -0
  116. package/dist/assets/storefront/public/next.svg +22 -0
  117. package/dist/assets/storefront/public/scenarios/b2b-wholesale/hero/hero-bg.webp +0 -0
  118. package/dist/assets/storefront/public/scenarios/b2b-wholesale/logo.svg +4 -0
  119. package/dist/assets/storefront/public/scenarios/default/logo.svg +4 -0
  120. package/dist/assets/storefront/public/scenarios/kaminpro/hero/hero-bg.webp +0 -0
  121. package/dist/assets/storefront/public/scenarios/kaminpro/logo.svg +7 -0
  122. package/dist/assets/storefront/public/vendure.svg +4 -0
  123. package/dist/assets/storefront/src/app/[locale]/(catalog)/catalog/page.tsx +65 -0
  124. package/dist/assets/storefront/src/app/[locale]/(catalog)/collection/[slug]/loading.tsx +24 -0
  125. package/dist/assets/storefront/src/app/[locale]/(catalog)/collection/[slug]/page.tsx +194 -0
  126. package/dist/assets/storefront/src/app/[locale]/(catalog)/layout.tsx +9 -0
  127. package/dist/assets/storefront/src/app/[locale]/(catalog)/product/[slug]/actions.ts +139 -0
  128. package/dist/assets/storefront/src/app/[locale]/(catalog)/product/[slug]/loading.tsx +46 -0
  129. package/dist/assets/storefront/src/app/[locale]/(catalog)/product/[slug]/page.tsx +330 -0
  130. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/actions.ts +11 -0
  131. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/loading.tsx +20 -0
  132. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/page.tsx +97 -0
  133. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/search-analytics-tracker.tsx +46 -0
  134. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/search-results.tsx +53 -0
  135. package/dist/assets/storefront/src/app/[locale]/(catalog)/search/search-term.tsx +26 -0
  136. package/dist/assets/storefront/src/app/[locale]/(scenario)/[...path]/page.tsx +31 -0
  137. package/dist/assets/storefront/src/app/[locale]/accept-invitation/[token]/accept-invitation-client.tsx +56 -0
  138. package/dist/assets/storefront/src/app/[locale]/accept-invitation/[token]/actions.ts +32 -0
  139. package/dist/assets/storefront/src/app/[locale]/accept-invitation/[token]/page.tsx +85 -0
  140. package/dist/assets/storefront/src/app/[locale]/accept-invitation/[token]/sign-out-and-retry.tsx +26 -0
  141. package/dist/assets/storefront/src/app/[locale]/account/addresses/actions.ts +125 -0
  142. package/dist/assets/storefront/src/app/[locale]/account/addresses/address-form.tsx +180 -0
  143. package/dist/assets/storefront/src/app/[locale]/account/addresses/addresses-client.tsx +292 -0
  144. package/dist/assets/storefront/src/app/[locale]/account/addresses/loading.tsx +48 -0
  145. package/dist/assets/storefront/src/app/[locale]/account/addresses/page.tsx +54 -0
  146. package/dist/assets/storefront/src/app/[locale]/account/approvals/actions.ts +28 -0
  147. package/dist/assets/storefront/src/app/[locale]/account/approvals/page.tsx +340 -0
  148. package/dist/assets/storefront/src/app/[locale]/account/company/actions.ts +33 -0
  149. package/dist/assets/storefront/src/app/[locale]/account/company/addresses/actions.ts +83 -0
  150. package/dist/assets/storefront/src/app/[locale]/account/company/addresses/address-form.tsx +267 -0
  151. package/dist/assets/storefront/src/app/[locale]/account/company/addresses/addresses-client.tsx +253 -0
  152. package/dist/assets/storefront/src/app/[locale]/account/company/addresses/page.tsx +75 -0
  153. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/actions.ts +70 -0
  154. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/business-unit-form.tsx +153 -0
  155. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/business-units-client.tsx +210 -0
  156. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/page.tsx +66 -0
  157. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/types.ts +8 -0
  158. package/dist/assets/storefront/src/app/[locale]/account/company/edit-company-form.tsx +57 -0
  159. package/dist/assets/storefront/src/app/[locale]/account/company/page.tsx +89 -0
  160. package/dist/assets/storefront/src/app/[locale]/account/company/payment-methods/page.tsx +65 -0
  161. package/dist/assets/storefront/src/app/[locale]/account/company/payment-methods/payment-methods-client.tsx +55 -0
  162. package/dist/assets/storefront/src/app/[locale]/account/company/roles/page.tsx +66 -0
  163. package/dist/assets/storefront/src/app/[locale]/account/company/roles/roles-client.tsx +93 -0
  164. package/dist/assets/storefront/src/app/[locale]/account/company/tax-ids/actions.ts +51 -0
  165. package/dist/assets/storefront/src/app/[locale]/account/company/tax-ids/page.tsx +57 -0
  166. package/dist/assets/storefront/src/app/[locale]/account/company/tax-ids/tax-ids-client.tsx +326 -0
  167. package/dist/assets/storefront/src/app/[locale]/account/company/users/actions.ts +74 -0
  168. package/dist/assets/storefront/src/app/[locale]/account/company/users/edit-user-form.tsx +157 -0
  169. package/dist/assets/storefront/src/app/[locale]/account/company/users/invite-user-form.tsx +133 -0
  170. package/dist/assets/storefront/src/app/[locale]/account/company/users/page.tsx +104 -0
  171. package/dist/assets/storefront/src/app/[locale]/account/company/users/users-client.tsx +266 -0
  172. package/dist/assets/storefront/src/app/[locale]/account/error.tsx +24 -0
  173. package/dist/assets/storefront/src/app/[locale]/account/layout.tsx +50 -0
  174. package/dist/assets/storefront/src/app/[locale]/account/orders/[code]/page.tsx +463 -0
  175. package/dist/assets/storefront/src/app/[locale]/account/orders/[code]/reorder-actions.ts +160 -0
  176. package/dist/assets/storefront/src/app/[locale]/account/orders/[code]/reorder-dialog.tsx +386 -0
  177. package/dist/assets/storefront/src/app/[locale]/account/orders/loading.tsx +45 -0
  178. package/dist/assets/storefront/src/app/[locale]/account/orders/page.tsx +309 -0
  179. package/dist/assets/storefront/src/app/[locale]/account/profile/actions.ts +106 -0
  180. package/dist/assets/storefront/src/app/[locale]/account/profile/change-password-form.tsx +82 -0
  181. package/dist/assets/storefront/src/app/[locale]/account/profile/edit-email-form.tsx +88 -0
  182. package/dist/assets/storefront/src/app/[locale]/account/profile/edit-profile-form.tsx +80 -0
  183. package/dist/assets/storefront/src/app/[locale]/account/profile/loading.tsx +54 -0
  184. package/dist/assets/storefront/src/app/[locale]/account/profile/page.tsx +44 -0
  185. package/dist/assets/storefront/src/app/[locale]/account/quick-order/actions.ts +103 -0
  186. package/dist/assets/storefront/src/app/[locale]/account/quick-order/file-upload-tab.tsx +148 -0
  187. package/dist/assets/storefront/src/app/[locale]/account/quick-order/manual-entry-tab.tsx +112 -0
  188. package/dist/assets/storefront/src/app/[locale]/account/quick-order/page.tsx +26 -0
  189. package/dist/assets/storefront/src/app/[locale]/account/quick-order/preview-table.tsx +170 -0
  190. package/dist/assets/storefront/src/app/[locale]/account/quick-order/quick-order-form.tsx +243 -0
  191. package/dist/assets/storefront/src/app/[locale]/account/quick-order/save-to-list-dialog.tsx +153 -0
  192. package/dist/assets/storefront/src/app/[locale]/account/quick-order/sku-autocomplete.tsx +118 -0
  193. package/dist/assets/storefront/src/app/[locale]/account/quick-order/text-input-tab.tsx +40 -0
  194. package/dist/assets/storefront/src/app/[locale]/account/quotes/[id]/page.tsx +260 -0
  195. package/dist/assets/storefront/src/app/[locale]/account/quotes/actions.ts +253 -0
  196. package/dist/assets/storefront/src/app/[locale]/account/quotes/new/new-quote-form.tsx +28 -0
  197. package/dist/assets/storefront/src/app/[locale]/account/quotes/new/page.tsx +51 -0
  198. package/dist/assets/storefront/src/app/[locale]/account/quotes/page.tsx +248 -0
  199. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/[id]/loading.tsx +44 -0
  200. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/[id]/page.tsx +51 -0
  201. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/[id]/shopping-list-detail-client.tsx +562 -0
  202. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/actions.ts +235 -0
  203. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/create-shopping-list-form.tsx +86 -0
  204. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/loading.tsx +48 -0
  205. package/dist/assets/storefront/src/app/[locale]/account/shopping-lists/page.tsx +176 -0
  206. package/dist/assets/storefront/src/app/[locale]/account/store-credits/actions.ts +31 -0
  207. package/dist/assets/storefront/src/app/[locale]/account/store-credits/claim-form.tsx +41 -0
  208. package/dist/assets/storefront/src/app/[locale]/account/store-credits/page.tsx +190 -0
  209. package/dist/assets/storefront/src/app/[locale]/account/store-credits/reveal-code.tsx +38 -0
  210. package/dist/assets/storefront/src/app/[locale]/account/verify-email/page.tsx +117 -0
  211. package/dist/assets/storefront/src/app/[locale]/cart/actions.ts +128 -0
  212. package/dist/assets/storefront/src/app/[locale]/cart/cart-items.tsx +228 -0
  213. package/dist/assets/storefront/src/app/[locale]/cart/cart-line-actions.tsx +55 -0
  214. package/dist/assets/storefront/src/app/[locale]/cart/cart-quantity-control.tsx +158 -0
  215. package/dist/assets/storefront/src/app/[locale]/cart/cart-remove-button.tsx +36 -0
  216. package/dist/assets/storefront/src/app/[locale]/cart/cart.tsx +151 -0
  217. package/dist/assets/storefront/src/app/[locale]/cart/code-input.tsx +49 -0
  218. package/dist/assets/storefront/src/app/[locale]/cart/loading.tsx +10 -0
  219. package/dist/assets/storefront/src/app/[locale]/cart/order-summary.tsx +178 -0
  220. package/dist/assets/storefront/src/app/[locale]/cart/page.tsx +33 -0
  221. package/dist/assets/storefront/src/app/[locale]/cart/promotion-code.tsx +142 -0
  222. package/dist/assets/storefront/src/app/[locale]/cart/reapproval-confirm.tsx +76 -0
  223. package/dist/assets/storefront/src/app/[locale]/cart/request-quote-button.tsx +80 -0
  224. package/dist/assets/storefront/src/app/[locale]/checkout/actions.ts +192 -0
  225. package/dist/assets/storefront/src/app/[locale]/checkout/cancel-quote-checkout-button.tsx +30 -0
  226. package/dist/assets/storefront/src/app/[locale]/checkout/checkout-flow.tsx +214 -0
  227. package/dist/assets/storefront/src/app/[locale]/checkout/checkout-provider.tsx +181 -0
  228. package/dist/assets/storefront/src/app/[locale]/checkout/loading.tsx +90 -0
  229. package/dist/assets/storefront/src/app/[locale]/checkout/order-summary.tsx +160 -0
  230. package/dist/assets/storefront/src/app/[locale]/checkout/page.tsx +154 -0
  231. package/dist/assets/storefront/src/app/[locale]/checkout/steps/delivery-step.tsx +107 -0
  232. package/dist/assets/storefront/src/app/[locale]/checkout/steps/payment-step.tsx +110 -0
  233. package/dist/assets/storefront/src/app/[locale]/checkout/steps/review-step.tsx +180 -0
  234. package/dist/assets/storefront/src/app/[locale]/checkout/steps/shipping-address-step.tsx +518 -0
  235. package/dist/assets/storefront/src/app/[locale]/checkout/types.ts +7 -0
  236. package/dist/assets/storefront/src/app/[locale]/error.tsx +28 -0
  237. package/dist/assets/storefront/src/app/[locale]/forgot-password/actions.ts +28 -0
  238. package/dist/assets/storefront/src/app/[locale]/forgot-password/forgot-password-form.tsx +128 -0
  239. package/dist/assets/storefront/src/app/[locale]/forgot-password/page.tsx +24 -0
  240. package/dist/assets/storefront/src/app/[locale]/globals.css +72 -0
  241. package/dist/assets/storefront/src/app/[locale]/layout.tsx +202 -0
  242. package/dist/assets/storefront/src/app/[locale]/not-found.tsx +26 -0
  243. package/dist/assets/storefront/src/app/[locale]/order-confirmation/[code]/order-confirmation.tsx +296 -0
  244. package/dist/assets/storefront/src/app/[locale]/order-confirmation/[code]/page.tsx +18 -0
  245. package/dist/assets/storefront/src/app/[locale]/page.tsx +125 -0
  246. package/dist/assets/storefront/src/app/[locale]/register/actions.ts +50 -0
  247. package/dist/assets/storefront/src/app/[locale]/register/page.tsx +82 -0
  248. package/dist/assets/storefront/src/app/[locale]/register/registration-form.tsx +237 -0
  249. package/dist/assets/storefront/src/app/[locale]/register-company/actions.ts +40 -0
  250. package/dist/assets/storefront/src/app/[locale]/register-company/page.tsx +43 -0
  251. package/dist/assets/storefront/src/app/[locale]/register-company/registration-form.tsx +48 -0
  252. package/dist/assets/storefront/src/app/[locale]/reset-password/actions.ts +40 -0
  253. package/dist/assets/storefront/src/app/[locale]/reset-password/page.tsx +32 -0
  254. package/dist/assets/storefront/src/app/[locale]/reset-password/reset-password-form.tsx +96 -0
  255. package/dist/assets/storefront/src/app/[locale]/shopping-list/[code]/add-to-cart-button.tsx +31 -0
  256. package/dist/assets/storefront/src/app/[locale]/shopping-list/[code]/loading.tsx +29 -0
  257. package/dist/assets/storefront/src/app/[locale]/shopping-list/[code]/page.tsx +157 -0
  258. package/dist/assets/storefront/src/app/[locale]/sign-in/actions.ts +76 -0
  259. package/dist/assets/storefront/src/app/[locale]/sign-in/login-form.tsx +177 -0
  260. package/dist/assets/storefront/src/app/[locale]/sign-in/page.tsx +81 -0
  261. package/dist/assets/storefront/src/app/[locale]/verify/actions.ts +41 -0
  262. package/dist/assets/storefront/src/app/[locale]/verify/page.tsx +40 -0
  263. package/dist/assets/storefront/src/app/[locale]/verify/verify-content.tsx +127 -0
  264. package/dist/assets/storefront/src/app/[locale]/verify-pending/page.tsx +67 -0
  265. package/dist/assets/storefront/src/app/api/clear-company-context/route.ts +26 -0
  266. package/dist/assets/storefront/src/app/api/draft/disable/route.ts +12 -0
  267. package/dist/assets/storefront/src/app/api/draft/route.ts +17 -0
  268. package/dist/assets/storefront/src/app/api/revalidate/route.ts +110 -0
  269. package/dist/assets/storefront/src/app/api/search-analytics/route.ts +16 -0
  270. package/dist/assets/storefront/src/app/favicon.ico +0 -0
  271. package/dist/assets/storefront/src/components/commerce/account-nav-items.ts +56 -0
  272. package/dist/assets/storefront/src/components/commerce/account-sidebar.tsx +112 -0
  273. package/dist/assets/storefront/src/components/commerce/active-filters.tsx +107 -0
  274. package/dist/assets/storefront/src/components/commerce/approval-decision-form.tsx +70 -0
  275. package/dist/assets/storefront/src/components/commerce/approval-status-badge.tsx +37 -0
  276. package/dist/assets/storefront/src/components/commerce/business-unit-switcher.tsx +56 -0
  277. package/dist/assets/storefront/src/components/commerce/catalog-search-bar.tsx +83 -0
  278. package/dist/assets/storefront/src/components/commerce/category-product-section.tsx +69 -0
  279. package/dist/assets/storefront/src/components/commerce/category-sidebar.tsx +133 -0
  280. package/dist/assets/storefront/src/components/commerce/collection-hero-banner.tsx +36 -0
  281. package/dist/assets/storefront/src/components/commerce/company-state-badge.tsx +32 -0
  282. package/dist/assets/storefront/src/components/commerce/company-switcher.tsx +58 -0
  283. package/dist/assets/storefront/src/components/commerce/configurator-form.tsx +606 -0
  284. package/dist/assets/storefront/src/components/commerce/facet-filters.tsx +286 -0
  285. package/dist/assets/storefront/src/components/commerce/gift-card-pdp.tsx +43 -0
  286. package/dist/assets/storefront/src/components/commerce/gift-card-product-info.tsx +137 -0
  287. package/dist/assets/storefront/src/components/commerce/highlighted-text.tsx +30 -0
  288. package/dist/assets/storefront/src/components/commerce/instant-search-dropdown.tsx +377 -0
  289. package/dist/assets/storefront/src/components/commerce/order-status-badge.tsx +41 -0
  290. package/dist/assets/storefront/src/components/commerce/packaging-unit-selector.tsx +146 -0
  291. package/dist/assets/storefront/src/components/commerce/permission-gate.tsx +20 -0
  292. package/dist/assets/storefront/src/components/commerce/price-range-filter.tsx +111 -0
  293. package/dist/assets/storefront/src/components/commerce/price-tiers.tsx +98 -0
  294. package/dist/assets/storefront/src/components/commerce/price.tsx +29 -0
  295. package/dist/assets/storefront/src/components/commerce/product-card-list.tsx +121 -0
  296. package/dist/assets/storefront/src/components/commerce/product-card.tsx +89 -0
  297. package/dist/assets/storefront/src/components/commerce/product-carousel.tsx +42 -0
  298. package/dist/assets/storefront/src/components/commerce/product-grid.tsx +54 -0
  299. package/dist/assets/storefront/src/components/commerce/product-image-carousel.tsx +127 -0
  300. package/dist/assets/storefront/src/components/commerce/product-info.tsx +525 -0
  301. package/dist/assets/storefront/src/components/commerce/product-list-expansion.tsx +393 -0
  302. package/dist/assets/storefront/src/components/commerce/product-list-view.tsx +34 -0
  303. package/dist/assets/storefront/src/components/commerce/product-sidebar.tsx +175 -0
  304. package/dist/assets/storefront/src/components/commerce/product-view-switcher.tsx +107 -0
  305. package/dist/assets/storefront/src/components/commerce/quote-action-buttons.tsx +235 -0
  306. package/dist/assets/storefront/src/components/commerce/quote-activity-timeline.tsx +320 -0
  307. package/dist/assets/storefront/src/components/commerce/quote-comment-form.tsx +52 -0
  308. package/dist/assets/storefront/src/components/commerce/quote-csv-export-button.tsx +97 -0
  309. package/dist/assets/storefront/src/components/commerce/quote-product-search.tsx +139 -0
  310. package/dist/assets/storefront/src/components/commerce/quote-request-form.tsx +252 -0
  311. package/dist/assets/storefront/src/components/commerce/quote-status-badge.tsx +41 -0
  312. package/dist/assets/storefront/src/components/commerce/quote-validity-indicator.tsx +33 -0
  313. package/dist/assets/storefront/src/components/commerce/quote-version-comparison.tsx +88 -0
  314. package/dist/assets/storefront/src/components/commerce/related-products.tsx +56 -0
  315. package/dist/assets/storefront/src/components/commerce/shop-by-category.tsx +59 -0
  316. package/dist/assets/storefront/src/components/commerce/shopping-list-toggle.tsx +158 -0
  317. package/dist/assets/storefront/src/components/commerce/sort-dropdown.tsx +51 -0
  318. package/dist/assets/storefront/src/components/commerce/store-credit-summary.tsx +45 -0
  319. package/dist/assets/storefront/src/components/commerce/subcollection-tiles.tsx +56 -0
  320. package/dist/assets/storefront/src/components/layout/auth-page-shell.tsx +15 -0
  321. package/dist/assets/storefront/src/components/layout/draft-mode-action.ts +8 -0
  322. package/dist/assets/storefront/src/components/layout/draft-mode-indicator.tsx +54 -0
  323. package/dist/assets/storefront/src/components/layout/draft-mode-link.tsx +29 -0
  324. package/dist/assets/storefront/src/components/layout/footer.tsx +168 -0
  325. package/dist/assets/storefront/src/components/layout/navbar/actions.ts +33 -0
  326. package/dist/assets/storefront/src/components/layout/navbar/cart-icon.tsx +50 -0
  327. package/dist/assets/storefront/src/components/layout/navbar/channel-picker-wrapper.tsx +19 -0
  328. package/dist/assets/storefront/src/components/layout/navbar/channel-picker.tsx +53 -0
  329. package/dist/assets/storefront/src/components/layout/navbar/currency-picker-wrapper.tsx +21 -0
  330. package/dist/assets/storefront/src/components/layout/navbar/currency-picker.tsx +52 -0
  331. package/dist/assets/storefront/src/components/layout/navbar/language-picker.tsx +51 -0
  332. package/dist/assets/storefront/src/components/layout/navbar/login-button.tsx +34 -0
  333. package/dist/assets/storefront/src/components/layout/navbar/mobile-nav.tsx +74 -0
  334. package/dist/assets/storefront/src/components/layout/navbar/nav-menu-items.tsx +51 -0
  335. package/dist/assets/storefront/src/components/layout/navbar/navbar-cart.tsx +5 -0
  336. package/dist/assets/storefront/src/components/layout/navbar/navbar-collections.tsx +291 -0
  337. package/dist/assets/storefront/src/components/layout/navbar/navbar-link.tsx +28 -0
  338. package/dist/assets/storefront/src/components/layout/navbar/navbar-user.tsx +147 -0
  339. package/dist/assets/storefront/src/components/layout/navbar/theme-switcher.tsx +39 -0
  340. package/dist/assets/storefront/src/components/layout/navbar.tsx +117 -0
  341. package/dist/assets/storefront/src/components/layout/search-input.tsx +71 -0
  342. package/dist/assets/storefront/src/components/providers/awaiting-approval-banner.tsx +58 -0
  343. package/dist/assets/storefront/src/components/providers/cart-count-server.tsx +31 -0
  344. package/dist/assets/storefront/src/components/providers/company-context-guard.tsx +47 -0
  345. package/dist/assets/storefront/src/components/providers/company-permissions-server.tsx +14 -0
  346. package/dist/assets/storefront/src/components/providers/theme-provider.tsx +16 -0
  347. package/dist/assets/storefront/src/components/shared/breadcrumbs.tsx +39 -0
  348. package/dist/assets/storefront/src/components/shared/catalog-sidebar-skeleton.tsx +21 -0
  349. package/dist/assets/storefront/src/components/shared/country-select.tsx +83 -0
  350. package/dist/assets/storefront/src/components/shared/error-state.tsx +16 -0
  351. package/dist/assets/storefront/src/components/shared/pagination.tsx +119 -0
  352. package/dist/assets/storefront/src/components/shared/prefetch-link.tsx +36 -0
  353. package/dist/assets/storefront/src/components/shared/product-grid-skeleton.tsx +26 -0
  354. package/dist/assets/storefront/src/components/shared/scenario-paused.tsx +80 -0
  355. package/dist/assets/storefront/src/components/shared/skeletons/cart-skeleton.tsx +48 -0
  356. package/dist/assets/storefront/src/components/shared/skeletons/navbar-user-skeleton.tsx +9 -0
  357. package/dist/assets/storefront/src/components/shared/skeletons/search-input-skeleton.tsx +7 -0
  358. package/dist/assets/storefront/src/components/shared/skeletons/search-results-skeleton.tsx +17 -0
  359. package/dist/assets/storefront/src/components/ui/accordion.tsx +66 -0
  360. package/dist/assets/storefront/src/components/ui/alert-dialog.tsx +157 -0
  361. package/dist/assets/storefront/src/components/ui/alert.tsx +66 -0
  362. package/dist/assets/storefront/src/components/ui/aspect-ratio.tsx +11 -0
  363. package/dist/assets/storefront/src/components/ui/avatar.tsx +53 -0
  364. package/dist/assets/storefront/src/components/ui/badge.tsx +46 -0
  365. package/dist/assets/storefront/src/components/ui/breadcrumb.tsx +109 -0
  366. package/dist/assets/storefront/src/components/ui/button.tsx +58 -0
  367. package/dist/assets/storefront/src/components/ui/calendar.tsx +213 -0
  368. package/dist/assets/storefront/src/components/ui/card.tsx +92 -0
  369. package/dist/assets/storefront/src/components/ui/carousel.tsx +241 -0
  370. package/dist/assets/storefront/src/components/ui/chart.tsx +357 -0
  371. package/dist/assets/storefront/src/components/ui/checkbox.tsx +32 -0
  372. package/dist/assets/storefront/src/components/ui/collapsible.tsx +33 -0
  373. package/dist/assets/storefront/src/components/ui/command.tsx +184 -0
  374. package/dist/assets/storefront/src/components/ui/context-menu.tsx +252 -0
  375. package/dist/assets/storefront/src/components/ui/dialog.tsx +143 -0
  376. package/dist/assets/storefront/src/components/ui/drawer.tsx +135 -0
  377. package/dist/assets/storefront/src/components/ui/dropdown-menu.tsx +257 -0
  378. package/dist/assets/storefront/src/components/ui/empty-state.tsx +37 -0
  379. package/dist/assets/storefront/src/components/ui/field.tsx +244 -0
  380. package/dist/assets/storefront/src/components/ui/form.tsx +167 -0
  381. package/dist/assets/storefront/src/components/ui/hover-card.tsx +44 -0
  382. package/dist/assets/storefront/src/components/ui/input-otp.tsx +77 -0
  383. package/dist/assets/storefront/src/components/ui/input.tsx +21 -0
  384. package/dist/assets/storefront/src/components/ui/kbd-shortcut.tsx +20 -0
  385. package/dist/assets/storefront/src/components/ui/label.tsx +24 -0
  386. package/dist/assets/storefront/src/components/ui/menubar.tsx +276 -0
  387. package/dist/assets/storefront/src/components/ui/navigation-menu.tsx +168 -0
  388. package/dist/assets/storefront/src/components/ui/pagination.tsx +127 -0
  389. package/dist/assets/storefront/src/components/ui/popover.tsx +48 -0
  390. package/dist/assets/storefront/src/components/ui/progress.tsx +31 -0
  391. package/dist/assets/storefront/src/components/ui/radio-group.tsx +45 -0
  392. package/dist/assets/storefront/src/components/ui/resizable.tsx +56 -0
  393. package/dist/assets/storefront/src/components/ui/scroll-area.tsx +58 -0
  394. package/dist/assets/storefront/src/components/ui/select.tsx +185 -0
  395. package/dist/assets/storefront/src/components/ui/separator.tsx +28 -0
  396. package/dist/assets/storefront/src/components/ui/sheet.tsx +139 -0
  397. package/dist/assets/storefront/src/components/ui/sidebar.tsx +728 -0
  398. package/dist/assets/storefront/src/components/ui/skeleton.tsx +13 -0
  399. package/dist/assets/storefront/src/components/ui/slider.tsx +63 -0
  400. package/dist/assets/storefront/src/components/ui/sonner.tsx +25 -0
  401. package/dist/assets/storefront/src/components/ui/switch.tsx +31 -0
  402. package/dist/assets/storefront/src/components/ui/table.tsx +116 -0
  403. package/dist/assets/storefront/src/components/ui/tabs.tsx +66 -0
  404. package/dist/assets/storefront/src/components/ui/textarea.tsx +18 -0
  405. package/dist/assets/storefront/src/components/ui/toggle-group.tsx +73 -0
  406. package/dist/assets/storefront/src/components/ui/toggle.tsx +47 -0
  407. package/dist/assets/storefront/src/components/ui/tooltip.tsx +61 -0
  408. package/dist/assets/storefront/src/config/branding.ts +4 -0
  409. package/dist/assets/storefront/src/config/scenario-meta.ts +46 -0
  410. package/dist/assets/storefront/src/config/scenario.ts +313 -0
  411. package/dist/assets/storefront/src/contexts/auth-context.tsx +35 -0
  412. package/dist/assets/storefront/src/graphql.ts +14 -0
  413. package/dist/assets/storefront/src/hooks/use-mobile.ts +19 -0
  414. package/dist/assets/storefront/src/i18n/locale-utils.ts +33 -0
  415. package/dist/assets/storefront/src/i18n/locales.ts +18 -0
  416. package/dist/assets/storefront/src/i18n/navigation.ts +35 -0
  417. package/dist/assets/storefront/src/i18n/request.ts +19 -0
  418. package/dist/assets/storefront/src/i18n/routing.ts +19 -0
  419. package/dist/assets/storefront/src/i18n/server.ts +16 -0
  420. package/dist/assets/storefront/src/lib/actions/switch-channel.ts +32 -0
  421. package/dist/assets/storefront/src/lib/actions/switch-currency.ts +25 -0
  422. package/dist/assets/storefront/src/lib/auth.ts +18 -0
  423. package/dist/assets/storefront/src/lib/backend-health.ts +38 -0
  424. package/dist/assets/storefront/src/lib/cart-count-context.tsx +59 -0
  425. package/dist/assets/storefront/src/lib/cart-guard-utils.ts +10 -0
  426. package/dist/assets/storefront/src/lib/cart-guard.ts +104 -0
  427. package/dist/assets/storefront/src/lib/channel-server.ts +28 -0
  428. package/dist/assets/storefront/src/lib/channel.ts +2 -0
  429. package/dist/assets/storefront/src/lib/company-context.ts +35 -0
  430. package/dist/assets/storefront/src/lib/company-permissions-context.tsx +40 -0
  431. package/dist/assets/storefront/src/lib/currency-server.ts +20 -0
  432. package/dist/assets/storefront/src/lib/currency.ts +2 -0
  433. package/dist/assets/storefront/src/lib/format.ts +48 -0
  434. package/dist/assets/storefront/src/lib/metadata.ts +85 -0
  435. package/dist/assets/storefront/src/lib/quantity.test.js +20 -0
  436. package/dist/assets/storefront/src/lib/quantity.ts +20 -0
  437. package/dist/assets/storefront/src/lib/search-bar-context.tsx +45 -0
  438. package/dist/assets/storefront/src/lib/search-helpers.ts +69 -0
  439. package/dist/assets/storefront/src/lib/use-recent-searches.ts +55 -0
  440. package/dist/assets/storefront/src/lib/utils.ts +6 -0
  441. package/dist/assets/storefront/src/lib/vendure/actions.ts +96 -0
  442. package/dist/assets/storefront/src/lib/vendure/api.ts +164 -0
  443. package/dist/assets/storefront/src/lib/vendure/approval-utils.ts +21 -0
  444. package/dist/assets/storefront/src/lib/vendure/cached.ts +71 -0
  445. package/dist/assets/storefront/src/lib/vendure/fragments.ts +213 -0
  446. package/dist/assets/storefront/src/lib/vendure/instant-search-action.ts +104 -0
  447. package/dist/assets/storefront/src/lib/vendure/mutations.ts +1136 -0
  448. package/dist/assets/storefront/src/lib/vendure/queries.ts +1406 -0
  449. package/dist/assets/storefront/src/middleware.ts +11 -0
  450. package/dist/assets/storefront/src/scenarios/b2b-wholesale/branding.ts +180 -0
  451. package/dist/assets/storefront/src/scenarios/b2b-wholesale/hero.tsx +122 -0
  452. package/dist/assets/storefront/src/scenarios/b2b-wholesale/index.ts +21 -0
  453. package/dist/assets/storefront/src/scenarios/b2b-wholesale/meta.ts +3 -0
  454. package/dist/assets/storefront/src/scenarios/default/branding.ts +148 -0
  455. package/dist/assets/storefront/src/scenarios/default/hero.tsx +107 -0
  456. package/dist/assets/storefront/src/scenarios/default/index.ts +12 -0
  457. package/dist/assets/storefront/src/scenarios/default/meta.ts +3 -0
  458. package/dist/assets/storefront/src/scenarios/kaminpro/auth-gate.tsx +36 -0
  459. package/dist/assets/storefront/src/scenarios/kaminpro/branding.ts +238 -0
  460. package/dist/assets/storefront/src/scenarios/kaminpro/cart-items.tsx +359 -0
  461. package/dist/assets/storefront/src/scenarios/kaminpro/hero.tsx +123 -0
  462. package/dist/assets/storefront/src/scenarios/kaminpro/index.ts +50 -0
  463. package/dist/assets/storefront/src/scenarios/kaminpro/meta.ts +9 -0
  464. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/banners.tsx +21 -0
  465. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/bom-basket.tsx +221 -0
  466. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/btn.tsx +38 -0
  467. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/chip.tsx +35 -0
  468. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/config-header.tsx +72 -0
  469. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/field-row.tsx +126 -0
  470. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/info-dot.tsx +38 -0
  471. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/login-notice.tsx +81 -0
  472. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/module-panel.tsx +144 -0
  473. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/quote-dialog.tsx +110 -0
  474. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/schematic.tsx +674 -0
  475. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/components/why-panel.tsx +66 -0
  476. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/configurator-pdp.tsx +43 -0
  477. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/configurator-shell.tsx +320 -0
  478. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/lib/data.ts +150 -0
  479. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/lib/types.ts +30 -0
  480. package/dist/assets/storefront/src/scenarios/kaminpro/pdp/styles.css +104 -0
  481. package/dist/assets/storefront/src/scenarios/kaminpro/shop-by-system.tsx +97 -0
  482. package/dist/assets/storefront/src/scenarios/kaminpro/system-finder/decision-tree.ts +261 -0
  483. package/dist/assets/storefront/src/scenarios/kaminpro/system-finder/page.tsx +21 -0
  484. package/dist/assets/storefront/src/scenarios/kaminpro/system-finder/wizard.tsx +600 -0
  485. package/dist/assets/storefront/src/schema.graphql.placeholder +5 -0
  486. package/dist/assets/storefront/tsconfig.json +37 -0
  487. package/dist/assets/templates/CLAUDE.md.hbs +63 -0
  488. package/dist/assets/templates/README.md.hbs +53 -0
  489. package/dist/assets/templates/custom-populate.ts.hbs +28 -0
  490. package/dist/assets/templates/docker-compose.yml.hbs +29 -0
  491. package/dist/assets/templates/env.hbs +44 -0
  492. package/dist/assets/templates/environment.d.ts.hbs +32 -0
  493. package/dist/assets/templates/gitignore.hbs +7 -0
  494. package/dist/assets/templates/index.ts.hbs +16 -0
  495. package/dist/assets/templates/npmrc.hbs +1 -0
  496. package/dist/assets/templates/populate.ts.hbs +27 -0
  497. package/dist/assets/templates/root-package.json.hbs +17 -0
  498. package/dist/assets/templates/server-package.json.hbs +31 -0
  499. package/dist/assets/templates/tsconfig.dashboard.json.hbs +17 -0
  500. package/dist/assets/templates/tsconfig.root.json.hbs +16 -0
  501. package/dist/assets/templates/tsconfig.server.json.hbs +20 -0
  502. package/dist/assets/templates/vendure-config.ts.hbs +80 -0
  503. package/dist/assets/templates/vite.config.mts.hbs +27 -0
  504. package/dist/bin/run.d.ts +2 -0
  505. package/dist/bin/run.js +79 -0
  506. package/dist/bin/run.js.map +1 -0
  507. package/dist/cli.d.ts +6 -0
  508. package/dist/cli.js +154 -0
  509. package/dist/cli.js.map +1 -0
  510. package/dist/git-init.d.ts +4 -0
  511. package/dist/git-init.js +60 -0
  512. package/dist/git-init.js.map +1 -0
  513. package/dist/index.d.ts +4 -0
  514. package/dist/index.js +13 -0
  515. package/dist/index.js.map +1 -0
  516. package/dist/plugin-registry.d.ts +10 -0
  517. package/dist/plugin-registry.js +485 -0
  518. package/dist/plugin-registry.js.map +1 -0
  519. package/dist/scaffolder.d.ts +10 -0
  520. package/dist/scaffolder.js +218 -0
  521. package/dist/scaffolder.js.map +1 -0
  522. package/dist/storefront-copier.d.ts +6 -0
  523. package/dist/storefront-copier.js +68 -0
  524. package/dist/storefront-copier.js.map +1 -0
  525. package/dist/template-renderer.d.ts +30 -0
  526. package/dist/template-renderer.js +177 -0
  527. package/dist/template-renderer.js.map +1 -0
  528. package/dist/types.d.ts +60 -0
  529. package/dist/types.js +3 -0
  530. package/dist/types.js.map +1 -0
  531. package/package.json +48 -0
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useState, useCallback, useEffect } from 'react';
4
+
5
+ interface CartCountContextType {
6
+ count: number;
7
+ setCount: (count: number) => void;
8
+ increment: () => void;
9
+ decrement: () => void;
10
+ awaitingApprovalOrderCode: string | null;
11
+ setAwaitingApprovalOrderCode: (code: string | null) => void;
12
+ }
13
+
14
+ const CartCountContext = createContext<CartCountContextType | null>(null);
15
+
16
+ export function CartCountProvider({ initialCount, children }: { initialCount: number; children: React.ReactNode }) {
17
+ const [count, setCount] = useState(initialCount);
18
+ const [awaitingApprovalOrderCode, setAwaitingApprovalOrderCode] = useState<string | null>(null);
19
+ const increment = useCallback(() => setCount(c => c + 1), []);
20
+ const decrement = useCallback(() => setCount(c => Math.max(0, c - 1)), []);
21
+
22
+ return (
23
+ <CartCountContext.Provider
24
+ value={{
25
+ count,
26
+ setCount,
27
+ increment,
28
+ decrement,
29
+ awaitingApprovalOrderCode,
30
+ setAwaitingApprovalOrderCode,
31
+ }}
32
+ >
33
+ {children}
34
+ </CartCountContext.Provider>
35
+ );
36
+ }
37
+
38
+ export function CartStatusSync({
39
+ count,
40
+ awaitingApprovalOrderCode,
41
+ }: {
42
+ count: number;
43
+ awaitingApprovalOrderCode: string | null;
44
+ }) {
45
+ const { setCount, setAwaitingApprovalOrderCode } = useCartCount();
46
+ useEffect(() => {
47
+ setCount(count);
48
+ }, [count, setCount]);
49
+ useEffect(() => {
50
+ setAwaitingApprovalOrderCode(awaitingApprovalOrderCode);
51
+ }, [awaitingApprovalOrderCode, setAwaitingApprovalOrderCode]);
52
+ return null;
53
+ }
54
+
55
+ export function useCartCount() {
56
+ const ctx = useContext(CartCountContext);
57
+ if (!ctx) throw new Error('useCartCount must be used within CartCountProvider');
58
+ return ctx;
59
+ }
@@ -0,0 +1,10 @@
1
+ import type { CartGuardResult } from './cart-guard';
2
+
3
+ /**
4
+ * Returns a user-facing error message if the guard indicates the order
5
+ * cannot be modified, or undefined if modifications are allowed.
6
+ */
7
+ export function getGuardError(guard: CartGuardResult): string | undefined {
8
+ if (guard.blocked) return 'Order is awaiting approval and cannot be modified';
9
+ if (guard.quoteLocked) return 'You have a quote checkout in progress. Complete checkout or cancel the quote checkout to modify your cart.';
10
+ }
@@ -0,0 +1,104 @@
1
+ 'use server';
2
+
3
+ import { client } from '@/lib/vendure/api';
4
+ import { GetActiveOrderQuery, GetApprovalHistoryForEntityQuery } from '@/lib/vendure/queries';
5
+ import { TransitionOrderToStateMutation } from '@/lib/vendure/mutations';
6
+
7
+ export interface CartGuardResult {
8
+ /** Whether the order was in a non-AddingItems state and had to be transitioned */
9
+ transitioned: boolean;
10
+ /** Whether this modification will invalidate a prior approval */
11
+ requiresReapproval: boolean;
12
+ /** Whether the order is awaiting approval and cannot be modified */
13
+ blocked: boolean;
14
+ /** Whether the order is locked because it was created from an accepted quote */
15
+ quoteLocked: boolean;
16
+ /** The source quote ID if the order is quote-locked */
17
+ sourceQuoteId?: string;
18
+ }
19
+
20
+ /**
21
+ * Check whether the latest approval-history entry for the given order
22
+ * has status APPROVED. Used by both the lightweight reapproval check
23
+ * and the full cart guard.
24
+ */
25
+ async function checkOrderApprovalStatus(orderId: string): Promise<boolean> {
26
+ const [approvalResult] = await client.query(
27
+ GetApprovalHistoryForEntityQuery,
28
+ { entityType: 'Order', entityId: orderId },
29
+ );
30
+ const history = [...(approvalResult?.data?.approvalHistoryForEntity ?? [])]
31
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
32
+ const latestApproval = history[0] ?? null;
33
+ return latestApproval?.status === 'APPROVED';
34
+ }
35
+
36
+ /**
37
+ * Lightweight check (no state transition) for whether the active order
38
+ * was previously approved. Use this to decide whether to show a confirmation
39
+ * dialog before calling cartGuard() + the actual mutation.
40
+ */
41
+ export async function checkCartRequiresReapproval(): Promise<boolean> {
42
+ const [queryResult, queryError] = await client.query(GetActiveOrderQuery, {});
43
+ if (queryError) return false;
44
+ const order = queryResult.data.activeOrder;
45
+ if (!order || order.state !== 'ArrangingPayment') return false;
46
+
47
+ return checkOrderApprovalStatus(order.id);
48
+ }
49
+
50
+ /**
51
+ * Single guard that all cart-modifying operations should call before mutating.
52
+ *
53
+ * - Ensures the order is in AddingItems state (transitions if needed)
54
+ * - Detects whether the order was previously approved (so the caller/UI can warn)
55
+ * - Blocks modifications to orders in AwaitingApproval state
56
+ * - Blocks modifications to quote-locked orders (accepted quote in checkout)
57
+ */
58
+ export async function cartGuard(): Promise<CartGuardResult> {
59
+ const [queryResult, queryError] = await client.query(GetActiveOrderQuery, {});
60
+ if (queryError) throw queryError;
61
+ const order = queryResult.data.activeOrder;
62
+
63
+ if (!order) {
64
+ return { transitioned: false, requiresReapproval: false, blocked: false, quoteLocked: false };
65
+ }
66
+
67
+ // Orders awaiting approval cannot be modified
68
+ if (order.state === 'AwaitingApproval') {
69
+ return { transitioned: false, requiresReapproval: false, blocked: true, quoteLocked: false };
70
+ }
71
+
72
+ // Quote-locked orders cannot be modified via the normal cart flow
73
+ const sourceQuoteId = order.customFields?.sourceQuoteId;
74
+ if (sourceQuoteId) {
75
+ return {
76
+ transitioned: false,
77
+ requiresReapproval: false,
78
+ blocked: false,
79
+ quoteLocked: true,
80
+ sourceQuoteId: String(sourceQuoteId),
81
+ };
82
+ }
83
+
84
+ // Check if a prior approval exists for this order
85
+ let requiresReapproval = false;
86
+ if (order.state === 'ArrangingPayment') {
87
+ requiresReapproval = await checkOrderApprovalStatus(order.id);
88
+ }
89
+
90
+ // Transition to AddingItems if needed
91
+ let transitioned = false;
92
+ if (order.state !== 'AddingItems') {
93
+ const [res, error] = await client.mutate(TransitionOrderToStateMutation, { state: 'AddingItems' });
94
+ if (error) throw error;
95
+ if (res.data.transitionOrderToState?.__typename === 'OrderStateTransitionError') {
96
+ throw new Error(
97
+ `Cannot modify order: unable to transition from ${res.data.transitionOrderToState.fromState} to AddingItems`,
98
+ );
99
+ }
100
+ transitioned = true;
101
+ }
102
+
103
+ return { transitioned, requiresReapproval, blocked: false, quoteLocked: false };
104
+ }
@@ -0,0 +1,28 @@
1
+ import {cookies} from 'next/headers';
2
+ import {CHANNEL_COOKIE} from './channel';
3
+ import {getChannelsConfig} from '@/config/scenario';
4
+
5
+ const FALLBACK_TOKEN = process.env.VENDURE_CHANNEL_TOKEN || process.env.NEXT_PUBLIC_VENDURE_CHANNEL_TOKEN;
6
+
7
+ /**
8
+ * Resolve the active channel token:
9
+ * 1. Cookie value (user's explicit choice, validated against scenario config)
10
+ * 2. Scenario default (channels.defaultToken)
11
+ * 3. VENDURE_CHANNEL_TOKEN env var (non-channel-switching scenarios)
12
+ */
13
+ export async function getActiveChannelToken(): Promise<string | undefined> {
14
+ const channelsConfig = getChannelsConfig();
15
+
16
+ // No channel-switching configured — fall through to static env var
17
+ if (!channelsConfig) return FALLBACK_TOKEN;
18
+
19
+ const jar = await cookies();
20
+ const cookie = jar.get(CHANNEL_COOKIE)?.value;
21
+
22
+ // Validate: cookie token must be one of the declared available tokens
23
+ if (cookie && channelsConfig.available.some(c => c.token === cookie)) {
24
+ return cookie;
25
+ }
26
+
27
+ return channelsConfig.defaultToken;
28
+ }
@@ -0,0 +1,2 @@
1
+ /** Cookie name for the user's preferred channel. */
2
+ export const CHANNEL_COOKIE = 'vendure-channel';
@@ -0,0 +1,35 @@
1
+ import {cookies} from 'next/headers';
2
+
3
+ const COMPANY_ID_COOKIE = 'vendure-company-id';
4
+ const BUSINESS_UNIT_ID_COOKIE = 'vendure-business-unit-id';
5
+
6
+ export async function setCompanyId(companyId: string) {
7
+ const cookieStore = await cookies();
8
+ cookieStore.set(COMPANY_ID_COOKIE, companyId);
9
+ }
10
+
11
+ export async function getCompanyId(): Promise<string | undefined> {
12
+ const cookieStore = await cookies();
13
+ return cookieStore.get(COMPANY_ID_COOKIE)?.value;
14
+ }
15
+
16
+ export async function setBusinessUnitId(businessUnitId: string) {
17
+ const cookieStore = await cookies();
18
+ cookieStore.set(BUSINESS_UNIT_ID_COOKIE, businessUnitId);
19
+ }
20
+
21
+ export async function getBusinessUnitId(): Promise<string | undefined> {
22
+ const cookieStore = await cookies();
23
+ return cookieStore.get(BUSINESS_UNIT_ID_COOKIE)?.value;
24
+ }
25
+
26
+ export async function clearBusinessUnitId() {
27
+ const cookieStore = await cookies();
28
+ cookieStore.delete(BUSINESS_UNIT_ID_COOKIE);
29
+ }
30
+
31
+ export async function clearCompanyContext() {
32
+ const cookieStore = await cookies();
33
+ cookieStore.delete(COMPANY_ID_COOKIE);
34
+ cookieStore.delete(BUSINESS_UNIT_ID_COOKIE);
35
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react';
4
+
5
+ interface CompanyPermissionsContextValue {
6
+ permissions: string[];
7
+ hasPermission: (permission: string) => boolean;
8
+ }
9
+
10
+ const CompanyPermissionsContext = createContext<CompanyPermissionsContextValue>({
11
+ permissions: [],
12
+ hasPermission: () => true, // Default: allow for non-company users
13
+ });
14
+
15
+ export function CompanyPermissionsProvider({
16
+ permissions,
17
+ children,
18
+ }: {
19
+ permissions: string[] | null;
20
+ children: ReactNode;
21
+ }) {
22
+ const value: CompanyPermissionsContextValue = {
23
+ permissions: permissions ?? [],
24
+ hasPermission: (permission: string) => {
25
+ // If no company context (null), allow everything (B2C user)
26
+ if (permissions === null) return true;
27
+ return permissions.includes(permission);
28
+ },
29
+ };
30
+
31
+ return (
32
+ <CompanyPermissionsContext.Provider value={value}>
33
+ {children}
34
+ </CompanyPermissionsContext.Provider>
35
+ );
36
+ }
37
+
38
+ export function useCompanyPermissions() {
39
+ return useContext(CompanyPermissionsContext);
40
+ }
@@ -0,0 +1,20 @@
1
+ import {cookies} from 'next/headers';
2
+ import {CURRENCY_COOKIE} from './currency';
3
+ import {getActiveChannelCached} from './vendure/cached';
4
+
5
+ /**
6
+ * Resolve the active currency code from the cookie, falling back to the
7
+ * channel's default currency.
8
+ */
9
+ export async function getActiveCurrencyCode(): Promise<string> {
10
+ const jar = await cookies();
11
+ const cookie = jar.get(CURRENCY_COOKIE)?.value;
12
+ if (cookie) return cookie;
13
+ // Dynamic import to avoid circular dependency: currencyMiddleware (api.ts)
14
+ // → getActiveCurrencyCode → channel-server → scenario.ts. Do NOT import
15
+ // anything from api.ts in channel-server.ts or this chain becomes circular.
16
+ const {getActiveChannelToken} = await import('./channel-server');
17
+ const channelToken = await getActiveChannelToken();
18
+ const channel = await getActiveChannelCached(channelToken);
19
+ return channel.defaultCurrencyCode;
20
+ }
@@ -0,0 +1,2 @@
1
+ /** Cookie name for the user's preferred currency. */
2
+ export const CURRENCY_COOKIE = 'vendure-currency';
@@ -0,0 +1,48 @@
1
+ import {toIntlLocale} from '@/i18n/locale-utils';
2
+
3
+ /**
4
+ * Format a price value
5
+ * @param price Price in cents (smallest currency unit)
6
+ * @param locale Routing locale (e.g. 'en', 'de')
7
+ * @param currencyCode ISO 4217 currency code (e.g. 'EUR', 'USD')
8
+ */
9
+ export function formatPrice(price: number, locale: string, currencyCode = 'EUR', options?: Intl.NumberFormatOptions): string {
10
+ return new Intl.NumberFormat(toIntlLocale(locale), {
11
+ style: 'currency',
12
+ currency: currencyCode,
13
+ ...options,
14
+ }).format(price / 100);
15
+ }
16
+
17
+ type DateFormat = 'short' | 'long';
18
+
19
+ /**
20
+ * Format a date string
21
+ * @param dateString ISO date string
22
+ * @param locale Routing locale (e.g. 'en', 'de')
23
+ * @param format 'short' (Jan 15, 2024) or 'long' (January 15, 2024)
24
+ */
25
+ export function formatDate(dateString: string, locale: string, format: DateFormat = 'short'): string {
26
+ const options: Intl.DateTimeFormatOptions = format === 'long'
27
+ ? { year: 'numeric', month: 'long', day: 'numeric' }
28
+ : { year: 'numeric', month: 'short', day: 'numeric' };
29
+
30
+ return new Date(dateString).toLocaleDateString(toIntlLocale(locale), options);
31
+ }
32
+
33
+ /**
34
+ * Format a date string with time
35
+ * @param dateString ISO date string
36
+ * @param locale Routing locale (e.g. 'en', 'de')
37
+ */
38
+ export function formatDateTime(dateString: string, locale: string): string {
39
+ return new Date(dateString).toLocaleString(toIntlLocale(locale), {
40
+ month: 'short', day: 'numeric', year: 'numeric',
41
+ hour: 'numeric', minute: '2-digit',
42
+ });
43
+ }
44
+
45
+ export function maskCode(code: string): string {
46
+ if (code.length <= 8) return code;
47
+ return `${code.slice(0, 4)}····${code.slice(-4)}`;
48
+ }
@@ -0,0 +1,85 @@
1
+ import type { Metadata } from 'next';
2
+ import { getBranding } from '@/config/branding';
3
+ import { routing } from '@/i18n/routing';
4
+
5
+ export const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME || getBranding().siteName;
6
+ export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
7
+
8
+ /**
9
+ * Truncate text to a maximum length, preserving word boundaries.
10
+ * Strips HTML tags and is ideal for meta descriptions (recommended 150-160 chars).
11
+ */
12
+ export function truncateDescription(
13
+ text: string | null | undefined,
14
+ maxLength = 155
15
+ ): string {
16
+ if (!text) return '';
17
+
18
+ // Strip HTML tags if present
19
+ const cleanText = text.replace(/<[^>]*>/g, '').trim();
20
+
21
+ if (cleanText.length <= maxLength) return cleanText;
22
+
23
+ // Find the last space before maxLength to avoid cutting words
24
+ const truncated = cleanText.substring(0, maxLength);
25
+ const lastSpaceIndex = truncated.lastIndexOf(' ');
26
+
27
+ return lastSpaceIndex > 0
28
+ ? truncated.substring(0, lastSpaceIndex) + '...'
29
+ : truncated + '...';
30
+ }
31
+
32
+ /**
33
+ * Build a canonical URL for a given path, optionally prefixed with locale.
34
+ */
35
+ export function buildCanonicalUrl(path: string, locale?: string): string {
36
+ const baseUrl = SITE_URL.replace(/\/$/, '');
37
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
38
+ const prefix = locale ? `/${locale}` : '';
39
+ return `${baseUrl}${prefix}${cleanPath}`;
40
+ }
41
+
42
+ /**
43
+ * Build hreflang alternates for all configured locales.
44
+ */
45
+ export function buildAlternates(path: string, currentLocale?: string): Metadata['alternates'] {
46
+ const languages: Record<string, string> = {};
47
+ for (const loc of routing.locales) {
48
+ languages[loc] = buildCanonicalUrl(path, loc);
49
+ }
50
+ return {
51
+ canonical: buildCanonicalUrl(path, currentLocale),
52
+ languages,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Build Open Graph image array from an image URL.
58
+ */
59
+ export function buildOgImages(
60
+ imageUrl: string | null | undefined,
61
+ alt?: string
62
+ ): NonNullable<Metadata['openGraph']>['images'] {
63
+ if (!imageUrl) return undefined;
64
+
65
+ return [
66
+ {
67
+ url: imageUrl,
68
+ alt: alt || 'Product image',
69
+ },
70
+ ];
71
+ }
72
+
73
+ /**
74
+ * Create noindex/nofollow robots config for protected pages.
75
+ */
76
+ export function noIndexRobots(): Metadata['robots'] {
77
+ return {
78
+ index: false,
79
+ follow: false,
80
+ googleBot: {
81
+ index: false,
82
+ follow: false,
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,20 @@
1
+ import {describe, expect, test} from 'bun:test';
2
+
3
+ import {MIN_ORDER_QUANTITY, normalizeQuantity, sanitizeQuantityInput} from './quantity.ts';
4
+
5
+ describe('quantity helpers', () => {
6
+ test('sanitizeQuantityInput keeps only digits for client-side entry', () => {
7
+ expect(sanitizeQuantityInput('12ab-3')).toBe('123');
8
+ });
9
+
10
+ test('normalizeQuantity enforces the minimum quantity', () => {
11
+ expect(normalizeQuantity('')).toBe(MIN_ORDER_QUANTITY);
12
+ expect(normalizeQuantity(0)).toBe(MIN_ORDER_QUANTITY);
13
+ expect(normalizeQuantity(-5)).toBe(MIN_ORDER_QUANTITY);
14
+ });
15
+
16
+ test('normalizeQuantity preserves valid positive integers', () => {
17
+ expect(normalizeQuantity('7')).toBe(7);
18
+ expect(normalizeQuantity(3.9)).toBe(3);
19
+ });
20
+ });
@@ -0,0 +1,20 @@
1
+ export const MIN_ORDER_QUANTITY = 1;
2
+
3
+ export function sanitizeQuantityInput(value: string) {
4
+ return value.replace(/[^0-9]/g, '');
5
+ }
6
+
7
+ export function normalizeQuantity(
8
+ value: number | string | null | undefined,
9
+ minimum: number = MIN_ORDER_QUANTITY,
10
+ ) {
11
+ const parsed = typeof value === 'number'
12
+ ? value
13
+ : Number.parseInt(value ?? '', 10);
14
+
15
+ if (!Number.isFinite(parsed)) {
16
+ return minimum;
17
+ }
18
+
19
+ return Math.max(minimum, Math.floor(parsed));
20
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
4
+
5
+ interface SearchBarContextType {
6
+ hasInlineSearch: boolean;
7
+ registerInlineSearch: () => void;
8
+ unregisterInlineSearch: () => void;
9
+ searchInputRef: React.RefObject<HTMLInputElement | null>;
10
+ }
11
+
12
+ const SearchBarContext = createContext<SearchBarContextType | null>(null);
13
+
14
+ export function SearchBarProvider({ children }: { children: React.ReactNode }) {
15
+ const [hasInlineSearch, setHasInlineSearch] = useState(false);
16
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
17
+
18
+ const registerInlineSearch = useCallback(() => setHasInlineSearch(true), []);
19
+ const unregisterInlineSearch = useCallback(() => setHasInlineSearch(false), []);
20
+
21
+ useEffect(() => {
22
+ function handleKeyDown(e: KeyboardEvent) {
23
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
24
+ e.preventDefault();
25
+ const input = searchInputRef.current
26
+ ?? document.querySelector<HTMLInputElement>('input[type="search"]');
27
+ input?.focus();
28
+ }
29
+ }
30
+ document.addEventListener('keydown', handleKeyDown);
31
+ return () => document.removeEventListener('keydown', handleKeyDown);
32
+ }, []);
33
+
34
+ return (
35
+ <SearchBarContext.Provider value={{ hasInlineSearch, registerInlineSearch, unregisterInlineSearch, searchInputRef }}>
36
+ {children}
37
+ </SearchBarContext.Provider>
38
+ );
39
+ }
40
+
41
+ export function useSearchBar() {
42
+ const ctx = useContext(SearchBarContext);
43
+ if (!ctx) throw new Error('useSearchBar must be used within SearchBarProvider');
44
+ return ctx;
45
+ }
@@ -0,0 +1,69 @@
1
+ export const STRUCTURAL_PARAMS = new Set(['q', 'sort', 'view']);
2
+ export const RESERVED_PARAMS = new Set([...STRUCTURAL_PARAMS, 'page', 'priceMin', 'priceMax', 'inStock']);
3
+
4
+ export interface SearchInputParams {
5
+ term?: string;
6
+ collectionSlug?: string;
7
+ take: number;
8
+ skip: number;
9
+ groupByProduct: boolean;
10
+ sort: { name?: 'ASC' | 'DESC'; price?: 'ASC' | 'DESC' };
11
+ facetsFilter?: { facets: Array<{ code: string; facetValueCodes: string[] }> };
12
+ priceRange?: { min: number; max: number };
13
+ inStock?: boolean;
14
+ prefixMode?: boolean;
15
+ }
16
+
17
+ interface BuildSearchInputOptions {
18
+ searchParams: { [key: string]: string | string[] | undefined };
19
+ collectionSlug?: string;
20
+ }
21
+
22
+ export function buildSearchInput({
23
+ searchParams,
24
+ collectionSlug,
25
+ }: BuildSearchInputOptions): SearchInputParams {
26
+ const page = Number(searchParams.page) || 1;
27
+ const take = 12;
28
+ const skip = (page - 1) * take;
29
+ const sort = (searchParams.sort as string) || 'name-asc';
30
+ const searchTerm = searchParams.q as string;
31
+
32
+ const facets: Array<{ code: string; facetValueCodes: string[] }> = [];
33
+ for (const [key, value] of Object.entries(searchParams)) {
34
+ if (RESERVED_PARAMS.has(key) || value == null) continue;
35
+ const strVal = Array.isArray(value) ? value[0] : value;
36
+ if (!strVal) continue;
37
+ facets.push({ code: key, facetValueCodes: strVal.split(',') });
38
+ }
39
+
40
+ const priceMin = Number(searchParams.priceMin);
41
+ const priceMax = Number(searchParams.priceMax);
42
+ const hasPriceRange = !isNaN(priceMin) && !isNaN(priceMax) && (priceMin > 0 || priceMax > 0);
43
+
44
+ const inStock = searchParams.inStock === 'true' ? true : undefined;
45
+
46
+ const sortMapping: Record<string, { name?: 'ASC' | 'DESC'; price?: 'ASC' | 'DESC' }> = {
47
+ 'name-asc': { name: 'ASC' },
48
+ 'name-desc': { name: 'DESC' },
49
+ 'price-asc': { price: 'ASC' },
50
+ 'price-desc': { price: 'DESC' },
51
+ };
52
+
53
+ return {
54
+ ...(searchTerm && { term: searchTerm }),
55
+ ...(collectionSlug && { collectionSlug }),
56
+ take,
57
+ skip,
58
+ groupByProduct: true,
59
+ prefixMode: true,
60
+ sort: sortMapping[sort] || sortMapping['name-asc'],
61
+ ...(facets.length > 0 && { facetsFilter: { facets } }),
62
+ ...(hasPriceRange && { priceRange: { min: priceMin, max: priceMax } }),
63
+ ...(inStock != null && { inStock }),
64
+ };
65
+ }
66
+
67
+ export function getCurrentPage(searchParams: { [key: string]: string | string[] | undefined }): number {
68
+ return Number(searchParams.page) || 1;
69
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+
5
+ const STORAGE_KEY = 'vendure-recent-searches';
6
+ const MAX_RECENT = 5;
7
+
8
+ function readFromStorage(): string[] {
9
+ if (typeof window === 'undefined') return [];
10
+ try {
11
+ const raw = localStorage.getItem(STORAGE_KEY);
12
+ if (!raw) return [];
13
+ const parsed = JSON.parse(raw);
14
+ return Array.isArray(parsed) ? parsed.filter((s): s is string => typeof s === 'string').slice(0, MAX_RECENT) : [];
15
+ } catch {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ function writeToStorage(searches: string[]) {
21
+ try {
22
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(searches.slice(0, MAX_RECENT)));
23
+ } catch {
24
+ // localStorage full or unavailable
25
+ }
26
+ }
27
+
28
+ export function useRecentSearches() {
29
+ const [searches, setSearches] = useState<string[]>(readFromStorage);
30
+
31
+ const addSearch = useCallback((term: string) => {
32
+ const trimmed = term.trim();
33
+ if (!trimmed) return;
34
+ setSearches(prev => {
35
+ const next = [trimmed, ...prev.filter(s => s !== trimmed)].slice(0, MAX_RECENT);
36
+ writeToStorage(next);
37
+ return next;
38
+ });
39
+ }, []);
40
+
41
+ const removeSearch = useCallback((term: string) => {
42
+ setSearches(prev => {
43
+ const next = prev.filter(s => s !== term);
44
+ writeToStorage(next);
45
+ return next;
46
+ });
47
+ }, []);
48
+
49
+ const clearSearches = useCallback(() => {
50
+ setSearches([]);
51
+ writeToStorage([]);
52
+ }, []);
53
+
54
+ return { searches, addSearch, removeSearch, clearSearches };
55
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }