@akinon/projectzero 2.0.0-beta.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/CHANGELOG.md +191 -17
  2. package/app-template/.env.example +3 -0
  3. package/app-template/.github/instructions/account.instructions.md +749 -0
  4. package/app-template/.github/instructions/checkout.instructions.md +678 -0
  5. package/app-template/.github/instructions/default.instructions.md +279 -0
  6. package/app-template/.github/instructions/edge-cases.instructions.md +73 -0
  7. package/app-template/.github/instructions/routing.instructions.md +603 -0
  8. package/app-template/.github/instructions/settings.instructions.md +338 -0
  9. package/app-template/.gitignore +3 -0
  10. package/app-template/AGENTS.md +7 -0
  11. package/app-template/CHANGELOG.md +2065 -232
  12. package/app-template/Procfile +1 -1
  13. package/app-template/akinon.json +1 -4
  14. package/app-template/build.sh +10 -0
  15. package/app-template/docs/advanced-usage.md +111 -0
  16. package/app-template/docs/plugins.md +60 -25
  17. package/app-template/docs/sentry-usage.md +35 -0
  18. package/app-template/jest.config.ts +2 -2
  19. package/app-template/next-env.d.ts +1 -0
  20. package/app-template/{next.config.ts → next.config.mjs} +6 -7
  21. package/app-template/package.json +58 -50
  22. package/app-template/postcss.config.mjs +1 -4
  23. package/app-template/public/amex.svg +12 -0
  24. package/app-template/public/apple-pay.svg +16 -0
  25. package/app-template/public/assets/images/product-placeholder-1.jpg +0 -0
  26. package/app-template/public/assets/images/product-placeholder-2.jpg +0 -0
  27. package/app-template/public/assets/images/product-placeholder-3.jpg +0 -0
  28. package/app-template/public/assets/images/product-placeholder-4.jpg +0 -0
  29. package/app-template/public/google-pay.svg +16 -0
  30. package/app-template/public/locales/en/account.json +9 -4
  31. package/app-template/public/locales/en/auth.json +6 -7
  32. package/app-template/public/locales/en/basket.json +6 -6
  33. package/app-template/public/locales/en/blog.json +7 -0
  34. package/app-template/public/locales/en/category.json +3 -1
  35. package/app-template/public/locales/en/checkout.json +17 -4
  36. package/app-template/public/locales/en/common.json +61 -3
  37. package/app-template/public/locales/en/forgot_password.json +6 -7
  38. package/app-template/public/locales/en/product.json +84 -4
  39. package/app-template/public/locales/tr/account.json +9 -4
  40. package/app-template/public/locales/tr/auth.json +16 -17
  41. package/app-template/public/locales/tr/basket.json +4 -4
  42. package/app-template/public/locales/tr/blog.json +7 -0
  43. package/app-template/public/locales/tr/category.json +3 -1
  44. package/app-template/public/locales/tr/checkout.json +48 -36
  45. package/app-template/public/locales/tr/common.json +60 -2
  46. package/app-template/public/locales/tr/forgot_password.json +12 -13
  47. package/app-template/public/locales/tr/product.json +82 -0
  48. package/app-template/public/logo.svg +3 -27
  49. package/app-template/public/mastercard.svg +14 -0
  50. package/app-template/public/masterpass-javascript-sdk-web.min.js +1 -0
  51. package/app-template/public/promotion-banner.jpg +0 -0
  52. package/app-template/public/shop-pay.svg +12 -0
  53. package/app-template/public/visa.svg +12 -0
  54. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/[...prettyurl]/page.tsx +11 -11
  55. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/layout.tsx +4 -3
  56. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/cancellation/page.tsx +13 -10
  57. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/page.tsx +73 -51
  58. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/page.tsx +1 -1
  59. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/profile/page.tsx +2 -2
  60. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/address/stores/page.tsx +2 -2
  61. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/page.tsx +1 -1
  62. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket/page.tsx +2 -2
  63. package/app-template/src/app/[pz]/blog/[slug]/page.tsx +120 -0
  64. package/app-template/src/app/[pz]/category/[pk]/page.tsx +37 -0
  65. package/app-template/src/app/[pz]/flat-page/[pk]/page.tsx +23 -0
  66. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/forms/[pk]/generate/page.tsx +2 -3
  67. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +93 -0
  68. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/page.tsx +2 -4
  69. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/layout.tsx +6 -11
  70. package/app-template/src/app/[pz]/list/page.tsx +26 -0
  71. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/not-found.tsx +5 -7
  72. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/page.tsx +6 -4
  73. package/app-template/src/app/[pz]/page.tsx +28 -0
  74. package/app-template/src/app/[pz]/pages/[slug]/page.tsx +19 -0
  75. package/app-template/src/app/[pz]/product/[pk]/page.tsx +102 -0
  76. package/app-template/src/app/[pz]/special-page/[pk]/page.tsx +35 -0
  77. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/email-set-primary/[[...id]]/page.tsx +3 -4
  78. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/registration/account-confirm-email/[[...id]]/page.tsx +3 -3
  79. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/reset/[[...id]]/page.tsx +41 -5
  80. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/[node]/route.ts +8 -6
  81. package/app-template/src/app/api/auth/[...nextauth]/route.ts +3 -0
  82. package/app-template/src/app/api/barcode-search/route.ts +1 -0
  83. package/app-template/src/app/api/cache/route.ts +1 -1
  84. package/app-template/src/app/api/form/[...id]/route.ts +1 -7
  85. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  86. package/app-template/src/app/api/logout/route.ts +1 -1
  87. package/app-template/src/app/api/product-categories/route.ts +1 -0
  88. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  89. package/app-template/src/app/api/similar-products/route.ts +1 -0
  90. package/app-template/src/app/api/theme-settings/route.ts +12 -0
  91. package/app-template/src/app/api/virtual-try-on/limited-categories/route.ts +1 -0
  92. package/app-template/src/app/api/virtual-try-on/route.ts +1 -0
  93. package/app-template/src/app/api/web-vitals/route.ts +1 -1
  94. package/app-template/src/assets/fonts/pz-icon.css +211 -49
  95. package/app-template/src/assets/fonts/pz-icon.eot +0 -0
  96. package/app-template/src/assets/fonts/pz-icon.html +486 -0
  97. package/app-template/src/assets/fonts/pz-icon.scss +373 -49
  98. package/app-template/src/assets/fonts/pz-icon.svg +215 -53
  99. package/app-template/src/assets/fonts/pz-icon.ttf +0 -0
  100. package/app-template/src/assets/fonts/pz-icon.woff +0 -0
  101. package/app-template/src/assets/fonts/pz-icon.woff2 +0 -0
  102. package/app-template/src/assets/globals.scss +8 -133
  103. package/app-template/src/assets/icons/arrow-right.svg +3 -0
  104. package/app-template/src/assets/icons/cart.svg +4 -12
  105. package/app-template/src/assets/icons/check.svg +2 -18
  106. package/app-template/src/assets/icons/chevron-down.svg +2 -7
  107. package/app-template/src/assets/icons/delete.svg +3 -0
  108. package/app-template/src/assets/icons/facebook.svg +2 -8
  109. package/app-template/src/assets/icons/fav-off.svg +5 -0
  110. package/app-template/src/assets/icons/fav-on.svg +5 -0
  111. package/app-template/src/assets/icons/filter-and-sort.svg +3 -0
  112. package/app-template/src/assets/icons/heart.svg +3 -0
  113. package/app-template/src/assets/icons/instagram.svg +2 -13
  114. package/app-template/src/assets/icons/materials.svg +3 -0
  115. package/app-template/src/assets/icons/person.svg +4 -0
  116. package/app-template/src/assets/icons/pinterest.svg +5 -11
  117. package/app-template/src/assets/icons/ruler.svg +3 -0
  118. package/app-template/src/assets/icons/search.svg +8 -11
  119. package/app-template/src/assets/icons/share.svg +2 -9
  120. package/app-template/src/assets/icons/snapchat.svg +3 -0
  121. package/app-template/src/assets/icons/tiktok.svg +3 -0
  122. package/app-template/src/assets/icons/tumblr.svg +6 -0
  123. package/app-template/src/assets/icons/twitter.svg +2 -10
  124. package/app-template/src/assets/icons/vimeo.svg +3 -0
  125. package/app-template/src/assets/icons/youtube.svg +3 -0
  126. package/app-template/src/assets/icons/zoom.svg +8 -0
  127. package/app-template/src/auth.ts +3 -0
  128. package/app-template/src/components/__tests__/badge.test.tsx +2 -2
  129. package/app-template/src/components/__tests__/link.test.tsx +2 -0
  130. package/app-template/src/components/accordion.tsx +48 -23
  131. package/app-template/src/components/action-tooltip.tsx +160 -0
  132. package/app-template/src/components/button.tsx +1 -1
  133. package/app-template/src/components/carousel-core.tsx +4 -11
  134. package/app-template/src/components/checkbox.tsx +2 -1
  135. package/app-template/src/components/currency-select.tsx +150 -4
  136. package/app-template/src/components/file-input.tsx +27 -7
  137. package/app-template/src/components/generate-form-fields.tsx +49 -10
  138. package/app-template/src/components/icon.tsx +5 -6
  139. package/app-template/src/components/index.ts +4 -1
  140. package/app-template/src/components/input.tsx +11 -5
  141. package/app-template/src/components/language-select.tsx +88 -2
  142. package/app-template/src/components/modal.tsx +34 -16
  143. package/app-template/src/components/pagination.tsx +133 -20
  144. package/app-template/src/components/price.tsx +1 -1
  145. package/app-template/src/components/pwa-tags.tsx +1 -0
  146. package/app-template/src/components/quantity-input.tsx +63 -0
  147. package/app-template/src/components/quantity-selector.tsx +215 -0
  148. package/app-template/src/components/route-handler.tsx +50 -0
  149. package/app-template/src/components/select.tsx +86 -54
  150. package/app-template/src/components/shimmer.tsx +1 -1
  151. package/app-template/src/components/types/index.ts +51 -1
  152. package/app-template/src/components/widget-content.tsx +323 -0
  153. package/app-template/src/data/server/theme.ts +70 -0
  154. package/app-template/src/hooks/use-fav-button.tsx +9 -10
  155. package/app-template/src/hooks/use-product-cart.ts +80 -0
  156. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  157. package/app-template/src/hooks/use-theme-settings.ts +42 -0
  158. package/app-template/src/lib/fonts.ts +149 -0
  159. package/app-template/src/plugins.js +12 -2
  160. package/app-template/src/{middleware.ts → proxy.ts} +3 -3
  161. package/app-template/src/redux/middlewares/category.ts +5 -4
  162. package/app-template/src/redux/store.ts +21 -1
  163. package/app-template/src/routes/index.ts +8 -7
  164. package/app-template/src/settings.js +6 -3
  165. package/app-template/src/types/hookform-resolvers-yup.d.ts +28 -0
  166. package/app-template/src/types/index.ts +74 -3
  167. package/app-template/src/types/next-auth.d.ts +2 -2
  168. package/app-template/src/types/widget.ts +169 -0
  169. package/app-template/src/utils/__tests__/theme-page-context.test.ts +145 -0
  170. package/app-template/src/utils/formatDate.ts +48 -0
  171. package/app-template/src/utils/styles.ts +71 -0
  172. package/app-template/src/utils/theme-page-context.ts +309 -0
  173. package/app-template/src/utils/variant-validation.ts +41 -0
  174. package/app-template/src/views/account/address-form.tsx +8 -4
  175. package/app-template/src/views/account/contact-form.tsx +148 -131
  176. package/app-template/src/views/account/content-header.tsx +4 -3
  177. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  178. package/app-template/src/views/account/favorite-item.tsx +1 -1
  179. package/app-template/src/views/account/order.tsx +11 -9
  180. package/app-template/src/views/account/orders/order-cancellation-item.tsx +1 -1
  181. package/app-template/src/views/account/orders/order-detail-header.tsx +1 -1
  182. package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +45 -38
  183. package/app-template/src/views/basket/basket-item.tsx +6 -1
  184. package/app-template/src/views/basket/summary.tsx +16 -0
  185. package/app-template/src/views/breadcrumb.tsx +2 -2
  186. package/app-template/src/views/category/category-banner.tsx +4 -23
  187. package/app-template/src/views/category/category-info.tsx +2 -1
  188. package/app-template/src/views/category/filters/filter-item.tsx +138 -42
  189. package/app-template/src/views/category/filters/index.tsx +1 -1
  190. package/app-template/src/views/category/layout.tsx +1 -0
  191. package/app-template/src/views/checkout/auth.tsx +64 -40
  192. package/app-template/src/views/checkout/layout/header.tsx +10 -6
  193. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +22 -6
  194. package/app-template/src/views/checkout/steps/payment/options/funds-transfer.tsx +25 -5
  195. package/app-template/src/views/checkout/steps/payment/options/loyalty.tsx +21 -2
  196. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +27 -5
  197. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +464 -0
  198. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
  199. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +33 -20
  200. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +2 -2
  201. package/app-template/src/views/checkout/summary.tsx +12 -2
  202. package/app-template/src/views/find-in-store/index.tsx +2 -2
  203. package/app-template/src/views/guest-login/index.tsx +62 -58
  204. package/app-template/src/views/header/action-menu.tsx +1 -1
  205. package/app-template/src/views/header/band.tsx +2 -2
  206. package/app-template/src/views/header/index.tsx +1 -1
  207. package/app-template/src/views/header/mini-basket.tsx +3 -3
  208. package/app-template/src/views/header/mobile-hamburger-button.tsx +5 -8
  209. package/app-template/src/views/header/mobile-menu.tsx +18 -6
  210. package/app-template/src/views/header/navbar.tsx +1 -1
  211. package/app-template/src/views/header/pwa-back-button.tsx +1 -1
  212. package/app-template/src/views/header/search/index.tsx +13 -3
  213. package/app-template/src/views/header/search/results.tsx +1 -1
  214. package/app-template/src/views/header/user-menu.tsx +1 -3
  215. package/app-template/src/views/login/index.tsx +66 -57
  216. package/app-template/src/views/otp-login/index.tsx +11 -6
  217. package/app-template/src/views/product/index.ts +1 -0
  218. package/app-template/src/views/product/layout.tsx +26 -6
  219. package/app-template/src/views/product/price-wrapper.tsx +3 -24
  220. package/app-template/src/views/product/product-actions.tsx +165 -0
  221. package/app-template/src/views/product/product-info.tsx +76 -238
  222. package/app-template/src/views/product/product-share.tsx +58 -0
  223. package/app-template/src/views/product/product-variants.tsx +26 -0
  224. package/app-template/src/views/product/slider.tsx +22 -1
  225. package/app-template/src/views/product/variant.tsx +69 -41
  226. package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
  227. package/app-template/src/views/register/index.tsx +31 -46
  228. package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
  229. package/app-template/src/views/share/index.tsx +9 -6
  230. package/app-template/src/views/widgets/home-hero-slider-content.tsx +41 -39
  231. package/app-template/src/widgets/flatpages/about-us/index.tsx +78 -0
  232. package/app-template/src/widgets/flatpages/blog-list/index.tsx +129 -0
  233. package/app-template/src/widgets/footer-info.tsx +1 -1
  234. package/app-template/src/widgets/footer-menu.tsx +7 -3
  235. package/app-template/src/widgets/footer-subscription/footer-subscription-form.tsx +17 -14
  236. package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
  237. package/app-template/src/widgets/home-stories-eng.tsx +43 -35
  238. package/app-template/src/widgets/index.ts +7 -0
  239. package/app-template/src/widgets/schemas/about-us.json +46 -0
  240. package/app-template/src/widgets/schemas/blog-list.json +37 -0
  241. package/app-template/src/widgets/schemas/blog.json +29 -0
  242. package/app-template/tailwind.config.js +155 -7
  243. package/app-template/tsconfig.json +29 -11
  244. package/codemods/migrate-auth-v5/index.js +339 -0
  245. package/codemods/migrate-auth-v5/transform.js +86 -0
  246. package/codemods/migrate-segments/index.js +591 -0
  247. package/codemods/update-tailwind-config/index.js +30 -0
  248. package/codemods/update-tailwind-config/transform.js +102 -0
  249. package/codemods/upgrade-to-2/index.js +549 -0
  250. package/commands/codemod.ts +0 -1
  251. package/commands/plugins.ts +111 -46
  252. package/dist/commands/codemod.js +0 -1
  253. package/dist/commands/plugins.js +104 -36
  254. package/package.json +3 -2
  255. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +0 -22
  256. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +0 -20
  257. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +0 -74
  258. package/app-template/src/app/[commerce]/[locale]/[currency]/list/page.tsx +0 -18
  259. package/app-template/src/app/[commerce]/[locale]/[currency]/page.tsx +0 -50
  260. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +0 -84
  261. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +0 -27
  262. package/app-template/src/pages/api/auth/[...nextauth].ts +0 -3
  263. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/address/page.tsx +0 -0
  264. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-email/page.tsx +0 -0
  265. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-password/page.tsx +0 -0
  266. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/contact/page.tsx +0 -0
  267. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/coupons/page.tsx +0 -0
  268. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/email-verification/page.tsx +0 -0
  269. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/faq/page.tsx +0 -0
  270. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/favourite-products/page.tsx +0 -0
  271. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/my-quotations/page.tsx +0 -0
  272. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/layout.tsx +0 -0
  273. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/page.tsx +0 -0
  274. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/anonymous-tracking/page.tsx +0 -0
  275. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/oauth-login/page.tsx +0 -0
  276. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket-b2b/page.tsx +0 -0
  277. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/category/[pk]/loading.tsx +0 -0
  278. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/client-root.tsx +0 -0
  279. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/contact-us/page.tsx +0 -0
  280. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/error.tsx +0 -0
  281. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/flat-page/[pk]/loading.tsx +0 -0
  282. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/group-product/[pk]/loading.tsx +0 -0
  283. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/loading.tsx +0 -0
  284. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/list/loading.tsx +0 -0
  285. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/checkout/page.tsx +0 -0
  286. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/layout.tsx +0 -0
  287. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/special-page/[pk]/loading.tsx +0 -0
  288. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/template.tsx +0 -0
  289. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/password/reset/page.tsx +0 -0
  290. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/route.ts +0 -0
@@ -0,0 +1,145 @@
1
+ import {
2
+ buildAccountPageContext,
3
+ buildBasketPageContext,
4
+ buildCustomPageContext,
5
+ buildHomepagePageContext,
6
+ buildListingPageContext,
7
+ buildProductPageContext
8
+ } from '../theme-page-context';
9
+
10
+ describe('theme-page-context utils', () => {
11
+ it('builds homepage and custom page contexts', () => {
12
+ const homepage = buildHomepagePageContext();
13
+ const customPage = buildCustomPageContext('summer-campaign');
14
+
15
+ expect(homepage.homepage.slug).toBe('homepage');
16
+ expect(customPage.customPage.slug).toBe('summer-campaign');
17
+ expect(customPage.customPage.title).toBe('Summer Campaign');
18
+ });
19
+
20
+ it('builds product page context with normalized product aliases', () => {
21
+ const productPageContext = buildProductPageContext({
22
+ product: {
23
+ pk: 1,
24
+ name: 'Spring Sneaker',
25
+ absolute_url: '/products/spring-sneaker',
26
+ productimage_set: [{ image: 'https://cdn.example.com/image.jpg' }],
27
+ price: '199.90',
28
+ retail_price: '249.90',
29
+ currency_type: 'TRY',
30
+ attributes_kwargs: {}
31
+ },
32
+ selected_variant: null,
33
+ variants: []
34
+ } as never);
35
+
36
+ expect(productPageContext.product.title).toBe('Spring Sneaker');
37
+ expect(productPageContext.product.url).toBe('/products/spring-sneaker');
38
+ expect(productPageContext.product.images[0].url).toBe(
39
+ 'https://cdn.example.com/image.jpg'
40
+ );
41
+ });
42
+
43
+ it('builds listing context aliases for category/search/plp style pages', () => {
44
+ const listingContext = buildListingPageContext({
45
+ data: {
46
+ category: {
47
+ name: 'Sneakers',
48
+ absolute_url: '/category/sneakers',
49
+ attributes: {
50
+ category_seo_title: {
51
+ value: {
52
+ text: 'Sneaker landing'
53
+ }
54
+ }
55
+ }
56
+ },
57
+ products: [
58
+ {
59
+ pk: 1,
60
+ name: 'Spring Sneaker',
61
+ absolute_url: '/products/spring-sneaker',
62
+ productimage_set: [{ image: 'https://cdn.example.com/image.jpg' }],
63
+ price: '199.90',
64
+ retail_price: '249.90',
65
+ currency_type: 'TRY',
66
+ attributes_kwargs: {}
67
+ }
68
+ ],
69
+ facets: [{ key: 'color', name: 'Color' }],
70
+ pagination: { count: 12 },
71
+ sorters: [{ label: 'Newest', value: 'newest' }],
72
+ search_text: 'sneaker'
73
+ } as never,
74
+ breadcrumbData: [{ label: 'Sneakers', path: '/category/sneakers' }] as never,
75
+ pageType: 'search',
76
+ path: '/list'
77
+ });
78
+
79
+ expect(listingContext.page.type).toBe('search');
80
+ expect(listingContext.category.name).toBe('Sneakers');
81
+ expect(listingContext.search.query).toBe('sneaker');
82
+ expect(listingContext.plp.products).toHaveLength(1);
83
+ expect(listingContext.listing.totalProducts).toBe(12);
84
+ });
85
+
86
+ it('builds basket and account contexts from live data', () => {
87
+ const basketContext = buildBasketPageContext({
88
+ basket: {
89
+ pk: 10,
90
+ total_quantity: 3,
91
+ total_amount: '799.90',
92
+ total_product_amount: '899.90',
93
+ total_discount_amount: '100.00',
94
+ voucher_code: 'SPRING',
95
+ discounts: [],
96
+ basketitem_set: [
97
+ {
98
+ id: 1,
99
+ quantity: 2,
100
+ price: '399.95',
101
+ retail_price: '449.95',
102
+ total_amount: '799.90',
103
+ unit_price: '399.95',
104
+ currency_type: 'TRY',
105
+ image: 'https://cdn.example.com/image.jpg',
106
+ attributes_kwargs: {
107
+ color: { label: 'Color', value: 'Black' }
108
+ },
109
+ product: {
110
+ pk: 1,
111
+ name: 'Spring Sneaker',
112
+ absolute_url: '/products/spring-sneaker',
113
+ productimage_set: [
114
+ { image: 'https://cdn.example.com/image.jpg' }
115
+ ],
116
+ price: '399.95',
117
+ retail_price: '449.95',
118
+ currency_type: 'TRY',
119
+ attributes_kwargs: {}
120
+ }
121
+ }
122
+ ]
123
+ } as never,
124
+ path: '/baskets/basket'
125
+ });
126
+
127
+ const accountContext = buildAccountPageContext({
128
+ path: '/account/profile',
129
+ profileInfo: {
130
+ first_name: 'Ada',
131
+ last_name: 'Lovelace',
132
+ email: 'ada@example.com'
133
+ },
134
+ sessionUser: {
135
+ name: 'Ada Lovelace',
136
+ email: 'ada@example.com'
137
+ }
138
+ });
139
+
140
+ expect(basketContext.basket.totalQuantity).toBe(3);
141
+ expect(basketContext.basket.items[0].title).toBe('Spring Sneaker');
142
+ expect(accountContext.account.user.fullName).toBe('Ada Lovelace');
143
+ expect(accountContext.account.route).toBe('/account/profile');
144
+ });
145
+ });
@@ -0,0 +1,48 @@
1
+ type Locale = 'en' | 'tr';
2
+
3
+ const monthNamesByLocale: Record<Locale, string[]> = {
4
+ en: [
5
+ 'JANUARY',
6
+ 'FEBRUARY',
7
+ 'MARCH',
8
+ 'APRIL',
9
+ 'MAY',
10
+ 'JUNE',
11
+ 'JULY',
12
+ 'AUGUST',
13
+ 'SEPTEMBER',
14
+ 'OCTOBER',
15
+ 'NOVEMBER',
16
+ 'DECEMBER'
17
+ ],
18
+ tr: [
19
+ 'OCAK',
20
+ 'ŞUBAT',
21
+ 'MART',
22
+ 'NİSAN',
23
+ 'MAYIS',
24
+ 'HAZİRAN',
25
+ 'TEMMUZ',
26
+ 'AĞUSTOS',
27
+ 'EYLÜL',
28
+ 'EKİM',
29
+ 'KASIM',
30
+ 'ARALIK'
31
+ ]
32
+ };
33
+
34
+ export const formatDateToMonthYear = (
35
+ dateString: string,
36
+ locale: Locale = 'en'
37
+ ): string => {
38
+ try {
39
+ const date = new Date(dateString);
40
+ const monthNames = monthNamesByLocale[locale];
41
+ const month = monthNames[date.getUTCMonth()];
42
+ const year = date.getUTCFullYear();
43
+
44
+ return `${month} ${year}`;
45
+ } catch {
46
+ return dateString;
47
+ }
48
+ };
@@ -0,0 +1,71 @@
1
+ export type ResponsiveSize = 'mobile' | 'tablet' | 'desktop';
2
+
3
+ const breakpointOrder: ResponsiveSize[] = ['mobile', 'tablet', 'desktop'];
4
+
5
+ export function getResponsiveStyle(
6
+ style: Record<string, Record<string, string>> = {},
7
+ currentBreakpoint: ResponsiveSize,
8
+ context?: 'slider-item'
9
+ ): Record<string, string> {
10
+ let result = style.default ? { ...style.default } : {};
11
+ let largestDefinedBreakpoint: ResponsiveSize | undefined;
12
+
13
+ for (let i = 0; i < breakpointOrder.length; i++) {
14
+ const breakpoint = breakpointOrder[i];
15
+
16
+ if (breakpoint === currentBreakpoint) break;
17
+ if (style[breakpoint] && Object.keys(style[breakpoint]).length > 0) {
18
+ largestDefinedBreakpoint = breakpoint;
19
+ break;
20
+ }
21
+ }
22
+
23
+ if (largestDefinedBreakpoint) {
24
+ const styles = Object.entries(style[largestDefinedBreakpoint]).reduce(
25
+ (acc, [key, value]) => {
26
+ if (
27
+ context === 'slider-item' &&
28
+ key === 'display' &&
29
+ value === 'none'
30
+ ) {
31
+ return acc;
32
+ }
33
+ acc[key] = value;
34
+ return acc;
35
+ },
36
+ {} as Record<string, string>
37
+ );
38
+
39
+ result = {
40
+ ...result,
41
+ ...styles
42
+ };
43
+ }
44
+
45
+ if (
46
+ style[currentBreakpoint] &&
47
+ Object.keys(style[currentBreakpoint]).length > 0
48
+ ) {
49
+ const styles = Object.entries(style[currentBreakpoint]).reduce(
50
+ (acc, [key, value]) => {
51
+ if (
52
+ context === 'slider-item' &&
53
+ key === 'display' &&
54
+ value === 'none'
55
+ ) {
56
+ return acc;
57
+ }
58
+ acc[key] = value;
59
+ return acc;
60
+ },
61
+ {} as Record<string, string>
62
+ );
63
+
64
+ result = {
65
+ ...result,
66
+ ...styles
67
+ };
68
+ }
69
+
70
+ return result;
71
+ }
@@ -0,0 +1,309 @@
1
+ import { ROUTES } from '@theme/routes';
2
+ import type {
3
+ Basket,
4
+ BreadcrumbResultType,
5
+ GetCategoryResponse,
6
+ Product,
7
+ ProductResult
8
+ } from '@akinon/next/types';
9
+
10
+ type UnknownRecord = Record<string, unknown>;
11
+
12
+ const toRecord = (value: unknown): UnknownRecord =>
13
+ value && typeof value === 'object' ? (value as UnknownRecord) : {};
14
+
15
+ const toStringValue = (value: unknown, fallback = ''): string =>
16
+ typeof value === 'string' ? value : fallback;
17
+
18
+ const humanizeSlug = (value: string) =>
19
+ value
20
+ .split('-')
21
+ .filter(Boolean)
22
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(' ');
24
+
25
+ const normalizeProductImages = (
26
+ product?: Product | null
27
+ ): Array<Record<string, unknown>> =>
28
+ (product?.productimage_set || []).map((item, index) => ({
29
+ ...item,
30
+ url: item.image,
31
+ alt: `${product?.name || 'Product'} ${index + 1}`
32
+ }));
33
+
34
+ const normalizeAttributeEntries = (
35
+ attributes: unknown
36
+ ): Array<Record<string, string>> => {
37
+ const record = toRecord(attributes);
38
+
39
+ return Object.values(record)
40
+ .map(entry => {
41
+ const typedEntry = toRecord(entry);
42
+ const label = toStringValue(typedEntry.label);
43
+ const value = toStringValue(typedEntry.value);
44
+
45
+ if (!label && !value) {
46
+ return null;
47
+ }
48
+
49
+ return { label, value };
50
+ })
51
+ .filter((entry): entry is { label: string; value: string } => entry !== null);
52
+ };
53
+
54
+ export const normalizeProductEntry = (
55
+ product?: Product | null
56
+ ): Record<string, unknown> | null => {
57
+ if (!product) {
58
+ return null;
59
+ }
60
+
61
+ const activePrice = toRecord((product as Product & UnknownRecord).active_price);
62
+ const images = normalizeProductImages(product);
63
+
64
+ return {
65
+ ...product,
66
+ title: product.name,
67
+ name: product.name,
68
+ url: product.absolute_url,
69
+ image: product.image || images[0]?.url || '',
70
+ images,
71
+ price: activePrice.price ?? product.price,
72
+ retailPrice: activePrice.retail_price ?? product.retail_price,
73
+ currency: activePrice.currency_type ?? product.currency_type ?? '',
74
+ description: product.description || '',
75
+ attributes: normalizeAttributeEntries(product.attributes_kwargs)
76
+ };
77
+ };
78
+
79
+ export const buildProductPageContext = (data: ProductResult) => {
80
+ const normalizedProduct = normalizeProductEntry(data.product);
81
+ const normalizedSelectedVariant = normalizeProductEntry(data.selected_variant);
82
+
83
+ return {
84
+ page: {
85
+ type: 'product',
86
+ title: normalizedProduct?.title || 'Product',
87
+ path: normalizedProduct?.url || ''
88
+ },
89
+ product: {
90
+ ...normalizedProduct,
91
+ selectedVariant: normalizedSelectedVariant,
92
+ selected_variant: normalizedSelectedVariant,
93
+ variants: data.variants || []
94
+ }
95
+ };
96
+ };
97
+
98
+ export const buildHomepagePageContext = () => ({
99
+ page: {
100
+ type: 'homepage',
101
+ title: 'Homepage',
102
+ slug: 'homepage',
103
+ path: ROUTES.HOME
104
+ },
105
+ homepage: {
106
+ title: 'Homepage',
107
+ slug: 'homepage',
108
+ path: ROUTES.HOME
109
+ }
110
+ });
111
+
112
+ export const buildCustomPageContext = (slug: string) => {
113
+ const title = humanizeSlug(slug) || 'Custom Page';
114
+ const path = `/pages/${slug}`;
115
+
116
+ return {
117
+ page: {
118
+ type: 'custom-page',
119
+ title,
120
+ slug,
121
+ path
122
+ },
123
+ customPage: {
124
+ title,
125
+ slug,
126
+ path
127
+ }
128
+ };
129
+ };
130
+
131
+ export const buildListingPageContext = ({
132
+ data,
133
+ breadcrumbData,
134
+ pageType,
135
+ path
136
+ }: {
137
+ data: GetCategoryResponse;
138
+ breadcrumbData?: BreadcrumbResultType[];
139
+ pageType: 'category' | 'search' | 'plp';
140
+ path?: string;
141
+ }) => {
142
+ const normalizedProducts = (data.products || [])
143
+ .map(product => normalizeProductEntry(product))
144
+ .filter((product): product is Record<string, unknown> => product !== null);
145
+ const categorySeoTitle = toStringValue(
146
+ data.category?.attributes?.category_seo_title?.value?.text
147
+ );
148
+ const effectivePath = path || data.category?.absolute_url || ROUTES.HOME;
149
+ const totalProducts =
150
+ Number(toRecord(data.pagination).count) || normalizedProducts.length;
151
+ const title =
152
+ pageType === 'search'
153
+ ? data.search_text
154
+ ? `Search results for ${data.search_text}`
155
+ : 'Search Results'
156
+ : data.category?.name || 'Product Listing';
157
+
158
+ const category = {
159
+ ...(data.category || {}),
160
+ title: data.category?.name || '',
161
+ name: data.category?.name || '',
162
+ url: data.category?.absolute_url || '',
163
+ description: categorySeoTitle,
164
+ seoTitle: categorySeoTitle,
165
+ facets: data.facets || [],
166
+ products: normalizedProducts
167
+ };
168
+
169
+ const listingBase = {
170
+ title,
171
+ path: effectivePath,
172
+ query: data.search_text || '',
173
+ products: normalizedProducts,
174
+ totalProducts,
175
+ resultCount: totalProducts,
176
+ facets: data.facets || [],
177
+ sorters: data.sorters || [],
178
+ pagination: data.pagination || {},
179
+ category
180
+ };
181
+
182
+ return {
183
+ page: {
184
+ type: pageType,
185
+ title,
186
+ path: effectivePath
187
+ },
188
+ category,
189
+ listing: {
190
+ ...listingBase,
191
+ breadcrumbs: breadcrumbData || []
192
+ },
193
+ search: {
194
+ ...listingBase,
195
+ title: data.search_text
196
+ ? `Search results for ${data.search_text}`
197
+ : 'Search Results'
198
+ },
199
+ plp: {
200
+ ...listingBase,
201
+ title: data.category?.name || 'Product Listing'
202
+ },
203
+ breadcrumbs: breadcrumbData || []
204
+ };
205
+ };
206
+
207
+ export const buildBasketPageContext = ({
208
+ basket,
209
+ path
210
+ }: {
211
+ basket?: Basket | null;
212
+ path?: string;
213
+ }) => {
214
+ const normalizedItems = (basket?.basketitem_set || []).map(item => {
215
+ const normalizedProduct = normalizeProductEntry(item.product);
216
+
217
+ return {
218
+ ...item,
219
+ title: normalizedProduct?.title || item.product?.name || '',
220
+ name: normalizedProduct?.name || item.product?.name || '',
221
+ url: normalizedProduct?.url || item.product?.absolute_url || '',
222
+ image:
223
+ item.image ||
224
+ (typeof normalizedProduct?.image === 'string'
225
+ ? normalizedProduct.image
226
+ : ''),
227
+ price: item.price || normalizedProduct?.price || '',
228
+ retailPrice: item.retail_price || normalizedProduct?.retailPrice || '',
229
+ totalAmount: item.total_amount || '',
230
+ unitPrice: item.unit_price || '',
231
+ currency: item.currency_type || normalizedProduct?.currency || '',
232
+ quantity: item.quantity,
233
+ attributes: normalizeAttributeEntries(
234
+ item.attributes_kwargs || item.product?.attributes_kwargs
235
+ ),
236
+ product: normalizedProduct
237
+ };
238
+ });
239
+
240
+ return {
241
+ page: {
242
+ type: 'basket',
243
+ title: 'Basket',
244
+ path: path || ROUTES.BASKET
245
+ },
246
+ basket: {
247
+ id: basket?.pk || null,
248
+ path: path || ROUTES.BASKET,
249
+ checkoutUrl: ROUTES.CHECKOUT,
250
+ continueShoppingUrl: ROUTES.HOME,
251
+ isEmpty: normalizedItems.length === 0,
252
+ itemCount: normalizedItems.length,
253
+ totalQuantity: basket?.total_quantity || 0,
254
+ totalAmount: basket?.total_amount || '',
255
+ totalProductAmount: basket?.total_product_amount || '',
256
+ totalDiscountAmount: basket?.total_discount_amount || '',
257
+ voucherCode: basket?.voucher_code || '',
258
+ discounts: basket?.discounts || [],
259
+ items: normalizedItems
260
+ }
261
+ };
262
+ };
263
+
264
+ export const buildAccountPageContext = ({
265
+ path,
266
+ profileInfo,
267
+ sessionUser
268
+ }: {
269
+ path?: string;
270
+ profileInfo?: UnknownRecord | null;
271
+ sessionUser?: UnknownRecord | null;
272
+ }) => {
273
+ const normalizedPath = path || ROUTES.ACCOUNT;
274
+ const firstName =
275
+ toStringValue(profileInfo?.first_name) ||
276
+ toStringValue(sessionUser?.firstName) ||
277
+ toStringValue(sessionUser?.name).split(' ')[0] ||
278
+ '';
279
+ const lastName =
280
+ toStringValue(profileInfo?.last_name) ||
281
+ toStringValue(sessionUser?.lastName) ||
282
+ toStringValue(sessionUser?.name).split(' ').slice(1).join(' ') ||
283
+ '';
284
+ const email =
285
+ toStringValue(profileInfo?.email) || toStringValue(sessionUser?.email);
286
+ const fullName =
287
+ [firstName, lastName].filter(Boolean).join(' ') ||
288
+ toStringValue(sessionUser?.name);
289
+
290
+ return {
291
+ page: {
292
+ type: 'account',
293
+ title: 'Account',
294
+ path: normalizedPath
295
+ },
296
+ account: {
297
+ path: normalizedPath,
298
+ route: normalizedPath,
299
+ user: {
300
+ firstName,
301
+ lastName,
302
+ fullName,
303
+ email,
304
+ phone: toStringValue(profileInfo?.phone),
305
+ gender: toStringValue(profileInfo?.gender)
306
+ }
307
+ }
308
+ };
309
+ };
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { Trans } from '@akinon/next/components/trans';
3
+ import { VariantType } from '@akinon/next/types';
4
+
5
+ export const isVariantSelectionComplete = (variants: VariantType[]): boolean => {
6
+ return variants?.every((variant) =>
7
+ variant?.options.some((opt) => opt.is_selected)
8
+ );
9
+ };
10
+
11
+ export const getUnselectedVariant = (variants: VariantType[]): VariantType | undefined => {
12
+ return variants.find((variant) =>
13
+ variant.options.every((opt) => !opt.is_selected)
14
+ );
15
+ };
16
+
17
+ export const createVariantErrorMessage = (unselectedVariant: VariantType) => {
18
+ const TransComponent = Trans as any;
19
+ return React.createElement(
20
+ TransComponent,
21
+ {
22
+ i18nKey: "product.please_select_variant",
23
+ components: {
24
+ VariantName: React.createElement('span', {}, unselectedVariant.attribute_name)
25
+ }
26
+ }
27
+ );
28
+ };
29
+
30
+ export const validateVariantSelection = (variants: VariantType[]) => {
31
+ const unselectedVariant = getUnselectedVariant(variants);
32
+
33
+ if (unselectedVariant) {
34
+ return {
35
+ isValid: false,
36
+ errorMessage: createVariantErrorMessage(unselectedVariant)
37
+ };
38
+ }
39
+
40
+ return { isValid: true, errorMessage: null };
41
+ };
@@ -259,23 +259,25 @@ export const AddressForm = (props: Props) => {
259
259
  {/* TODO: Fix select and textarea components */}
260
260
 
261
261
  <Select
262
- className="w-full border-gray-500 text-sm mt-2"
262
+ className="w-full border-gray-500 text-sm"
263
263
  options={countryOptions}
264
264
  {...register('country')}
265
265
  error={errors.country}
266
266
  data-testid="address-form-country"
267
267
  label={t('account.address_book.form.country.title')}
268
+ labelClassName="mb-3"
268
269
  required
269
270
  />
270
271
 
271
272
  {city && (
272
273
  <Select
273
- className="w-full border-gray-500 text-sm mt-2"
274
+ className="w-full border-gray-500 text-sm"
274
275
  options={cityOptions}
275
276
  {...register('city')}
276
277
  error={errors.city}
277
278
  data-testid="address-form-city"
278
279
  label={t('account.address_book.form.province.title')}
280
+ labelClassName="mb-3"
279
281
  required
280
282
  />
281
283
  )}
@@ -283,24 +285,26 @@ export const AddressForm = (props: Props) => {
283
285
  <div className="flex gap-4">
284
286
  <div className="flex-1">
285
287
  <Select
286
- className="w-full border-gray-500 text-sm mt-2"
288
+ className="w-full border-gray-500 text-sm"
287
289
  options={townshipOptions}
288
290
  {...register('township')}
289
291
  error={errors.township}
290
292
  data-testid="address-form-township"
291
293
  label={t('account.address_book.form.township.title')}
294
+ labelClassName="mb-3"
292
295
  required
293
296
  />
294
297
  </div>
295
298
  {district && (
296
299
  <div className="flex-1">
297
300
  <Select
298
- className="w-full border-gray-500 text-sm mt-2"
301
+ className="w-full border-gray-500 text-sm"
299
302
  options={districtOptions}
300
303
  {...register('district')}
301
304
  error={errors.district}
302
305
  data-testid="address-form-district"
303
306
  label={t('account.address_book.form.district.title')}
307
+ labelClassName="mb-3"
304
308
  required
305
309
  />
306
310
  </div>