tanstack_start_ts 1.0.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 (336) hide show
  1. package/.wrangler/deploy/config.json +1 -0
  2. package/bunfig.toml +6 -0
  3. package/components.json +22 -0
  4. package/config.json +0 -0
  5. package/dist/client/.assetsignore +2 -0
  6. package/dist/client/assets/ProductCard-DbIkJAE-.js +1 -0
  7. package/dist/client/assets/about-AskxOruL.js +1 -0
  8. package/dist/client/assets/admin-BZVcAQM3.js +1 -0
  9. package/dist/client/assets/admin.functions--RdVcuBx.js +1 -0
  10. package/dist/client/assets/admin.login-QgrF_9Fp.js +1 -0
  11. package/dist/client/assets/affiliate-disclosure-BIAsA-HO.js +1 -0
  12. package/dist/client/assets/categories-D0N418mK.js +1 -0
  13. package/dist/client/assets/category._slug-aCaQm14E.js +1 -0
  14. package/dist/client/assets/contact-PhvO-V15.js +1 -0
  15. package/dist/client/assets/faq-BsiHWPM8.js +1 -0
  16. package/dist/client/assets/hero-bg-BP2eVUIX.jpg +0 -0
  17. package/dist/client/assets/index-BU9rnkF3.js +1 -0
  18. package/dist/client/assets/index-BpJWZkva.js +1 -0
  19. package/dist/client/assets/index-vRX-zAyq.js +1 -0
  20. package/dist/client/assets/login-DteE0ZGp.js +1 -0
  21. package/dist/client/assets/logo-pSNfLJQk.png +0 -0
  22. package/dist/client/assets/privacy-B_Pu7040.js +1 -0
  23. package/dist/client/assets/product-links-BkZ41Gv3.js +1 -0
  24. package/dist/client/assets/product._id-BVUysCW-.js +1 -0
  25. package/dist/client/assets/products.functions-cGzRziKO.js +1 -0
  26. package/dist/client/assets/profile-CveRcKq2.js +1 -0
  27. package/dist/client/assets/reset-password-ySEjItX_.js +1 -0
  28. package/dist/client/assets/saved-CHtdQDJF.js +1 -0
  29. package/dist/client/assets/search-CXWfET1y.js +1 -0
  30. package/dist/client/assets/signup-CEx90iuV.js +1 -0
  31. package/dist/client/assets/styles-DrNJG0BO.css +1 -0
  32. package/dist/client/assets/terms-VqJ9kX9b.js +1 -0
  33. package/dist/client/assets/update-password-C-d0ix5e.js +1 -0
  34. package/dist/client/assets/vendor-aria-hidden-DvXkyWUv.js +1 -0
  35. package/dist/client/assets/vendor-class-variance-authority-5VPnzWs2.js +1 -0
  36. package/dist/client/assets/vendor-clsx-B-dksMZM.js +1 -0
  37. package/dist/client/assets/vendor-cookie-es-CS0aJGDi.js +1 -0
  38. package/dist/client/assets/vendor-detect-node-es-l0sNRNKZ.js +1 -0
  39. package/dist/client/assets/vendor-floating-ui-core-BlUy28sp.js +1 -0
  40. package/dist/client/assets/vendor-floating-ui-dom-BxK0hn2R.js +1 -0
  41. package/dist/client/assets/vendor-floating-ui-react-dom-Bas3975S.js +1 -0
  42. package/dist/client/assets/vendor-floating-ui-utils-BfYUAVcw.js +1 -0
  43. package/dist/client/assets/vendor-framer-motion-BMdL-cuX.js +9 -0
  44. package/dist/client/assets/vendor-get-nonce-C-Z93AgS.js +1 -0
  45. package/dist/client/assets/vendor-iceberg-js-tWD4K6Lg.js +1 -0
  46. package/dist/client/assets/vendor-lovable.dev-cloud-auth-js-VuzqtJVg.js +1 -0
  47. package/dist/client/assets/vendor-lucide-react-b5K2fehp.js +1 -0
  48. package/dist/client/assets/vendor-motion-dom-BETJamZt.js +1 -0
  49. package/dist/client/assets/vendor-motion-utils-BuWewJbj.js +1 -0
  50. package/dist/client/assets/vendor-radix-ui-primitive-Dc_FVRD7.js +1 -0
  51. package/dist/client/assets/vendor-radix-ui-react-accordion-C22Rgxe9.js +1 -0
  52. package/dist/client/assets/vendor-radix-ui-react-arrow-DMHj2mKI.js +1 -0
  53. package/dist/client/assets/vendor-radix-ui-react-avatar-CVPBkFXg.js +1 -0
  54. package/dist/client/assets/vendor-radix-ui-react-collapsible-BvM-4sKX.js +1 -0
  55. package/dist/client/assets/vendor-radix-ui-react-collection-D9KtqmHm.js +1 -0
  56. package/dist/client/assets/vendor-radix-ui-react-compose-refs-Cvq0AS8Z.js +1 -0
  57. package/dist/client/assets/vendor-radix-ui-react-context-CAqqn5Nx.js +1 -0
  58. package/dist/client/assets/vendor-radix-ui-react-dialog-DZ01vOLq.js +5 -0
  59. package/dist/client/assets/vendor-radix-ui-react-direction-DxZwNuei.js +1 -0
  60. package/dist/client/assets/vendor-radix-ui-react-dismissable-layer-Dqgrs55Y.js +1 -0
  61. package/dist/client/assets/vendor-radix-ui-react-dropdown-menu-0uzvrqkn.js +1 -0
  62. package/dist/client/assets/vendor-radix-ui-react-focus-guards-DgWoZ-fP.js +1 -0
  63. package/dist/client/assets/vendor-radix-ui-react-focus-scope-BLIu5QaL.js +1 -0
  64. package/dist/client/assets/vendor-radix-ui-react-id-bpga_rLa.js +1 -0
  65. package/dist/client/assets/vendor-radix-ui-react-menu-D0qf2r6_.js +1 -0
  66. package/dist/client/assets/vendor-radix-ui-react-popper-BafIylxU.js +1 -0
  67. package/dist/client/assets/vendor-radix-ui-react-portal-BnAsfNCS.js +1 -0
  68. package/dist/client/assets/vendor-radix-ui-react-presence-C-f3UKQ2.js +1 -0
  69. package/dist/client/assets/vendor-radix-ui-react-primitive-zTHwXNoz.js +1 -0
  70. package/dist/client/assets/vendor-radix-ui-react-roving-focus-jyJB8K2E.js +1 -0
  71. package/dist/client/assets/vendor-radix-ui-react-slot-6LXHJrHl.js +1 -0
  72. package/dist/client/assets/vendor-radix-ui-react-use-callback-ref-E91aPc6s.js +1 -0
  73. package/dist/client/assets/vendor-radix-ui-react-use-controllable-state-Ca3eMtxa.js +1 -0
  74. package/dist/client/assets/vendor-radix-ui-react-use-effect-event-CPeX4A3c.js +1 -0
  75. package/dist/client/assets/vendor-radix-ui-react-use-escape-keydown-7n3YsXFo.js +1 -0
  76. package/dist/client/assets/vendor-radix-ui-react-use-is-hydrated-C1PY1qNv.js +1 -0
  77. package/dist/client/assets/vendor-radix-ui-react-use-layout-effect-B3AcGWPy.js +1 -0
  78. package/dist/client/assets/vendor-radix-ui-react-use-size-CXS04sct.js +1 -0
  79. package/dist/client/assets/vendor-react-dom-BnNs-kzm.js +9 -0
  80. package/dist/client/assets/vendor-react-gJPiVnX5.js +1 -0
  81. package/dist/client/assets/vendor-react-remove-scroll-DHKl-IMP.js +4 -0
  82. package/dist/client/assets/vendor-react-remove-scroll-bar-CSjdInc2.js +38 -0
  83. package/dist/client/assets/vendor-react-style-singleton-BqHpkgXn.js +1 -0
  84. package/dist/client/assets/vendor-scheduler-7OC5HNn7.js +1 -0
  85. package/dist/client/assets/vendor-seroval-B_Fur-nl.js +3 -0
  86. package/dist/client/assets/vendor-seroval-plugins-CBHnPkZJ.js +1 -0
  87. package/dist/client/assets/vendor-sonner-71-LdGG1.js +1 -0
  88. package/dist/client/assets/vendor-supabase-auth-js-DWrN-bIx.js +18 -0
  89. package/dist/client/assets/vendor-supabase-functions-js-uY_V-TxC.js +1 -0
  90. package/dist/client/assets/vendor-supabase-phoenix-BzEf37Ve.js +2 -0
  91. package/dist/client/assets/vendor-supabase-postgrest-js-C4rBWbCx.js +4 -0
  92. package/dist/client/assets/vendor-supabase-realtime-js-D6BlOYKE.js +23 -0
  93. package/dist/client/assets/vendor-supabase-storage-js-BG98L3Zz.js +1 -0
  94. package/dist/client/assets/vendor-supabase-supabase-js-DCCzdwBJ.js +1 -0
  95. package/dist/client/assets/vendor-tailwind-merge-Ct12j0u0.js +1 -0
  96. package/dist/client/assets/vendor-tanstack-history-C617CaxG.js +1 -0
  97. package/dist/client/assets/vendor-tanstack-query-core-7wuJJ5ZL.js +1 -0
  98. package/dist/client/assets/vendor-tanstack-react-query-HImzo8sX.js +1 -0
  99. package/dist/client/assets/vendor-tanstack-react-router-sIZLK-LU.js +1 -0
  100. package/dist/client/assets/vendor-tanstack-react-start-client-GiYCfWmf.js +1 -0
  101. package/dist/client/assets/vendor-tanstack-react-store-EvTi3ahh.js +1 -0
  102. package/dist/client/assets/vendor-tanstack-router-core-Cr7bYUZv.js +1 -0
  103. package/dist/client/assets/vendor-tanstack-start-client-core-C-00BBOu.js +2 -0
  104. package/dist/client/assets/vendor-tanstack-start-fn-stubs-l0sNRNKZ.js +1 -0
  105. package/dist/client/assets/vendor-tanstack-store-BC7mA7pq.js +1 -0
  106. package/dist/client/assets/vendor-tslib-Du-meQkk.js +1 -0
  107. package/dist/client/assets/vendor-use-callback-ref-C_fIAtot.js +1 -0
  108. package/dist/client/assets/vendor-use-sidecar-Bh0DDN6h.js +1 -0
  109. package/dist/client/assets/vendor-use-sync-external-store-ZvKHXaIn.js +1 -0
  110. package/dist/client/assets/vendor-vercel-analytics-DwPM5BWs.js +1 -0
  111. package/dist/client/assets/vendor-zod-By9teAtI.js +1 -0
  112. package/dist/client/robots.txt +2 -0
  113. package/dist/server/.dev.vars +5 -0
  114. package/dist/server/.vite/manifest.json +2528 -0
  115. package/dist/server/assets/ProductCard-CUPXy5Eo.js +149 -0
  116. package/dist/server/assets/_tanstack-start-manifest_v-do7vTWFD.js +4 -0
  117. package/dist/server/assets/about-TfKQw0Ga.js +28 -0
  118. package/dist/server/assets/admin-DspfJOJk.js +578 -0
  119. package/dist/server/assets/admin.functions-B78ppWLR.js +645 -0
  120. package/dist/server/assets/admin.functions-BWlKBoTL.js +93 -0
  121. package/dist/server/assets/admin.login-CV7QfeA6.js +139 -0
  122. package/dist/server/assets/affiliate-disclosure-B1wI1cDb.js +86 -0
  123. package/dist/server/assets/auth-middleware-Cn49MidW.js +62 -0
  124. package/dist/server/assets/categories-Z7jnAYZP.js +108 -0
  125. package/dist/server/assets/category._slug-D0XY3FGK.js +112 -0
  126. package/dist/server/assets/contact-IzyONsXs.js +104 -0
  127. package/dist/server/assets/faq-aRhB_CR3.js +133 -0
  128. package/dist/server/assets/hero-bg-BP2eVUIX.jpg +0 -0
  129. package/dist/server/assets/index-BTPHbXw9.js +221 -0
  130. package/dist/server/assets/index-ByJkHkrU.js +30 -0
  131. package/dist/server/assets/login-Dvy5Dm0f.js +175 -0
  132. package/dist/server/assets/logo-pSNfLJQk.png +0 -0
  133. package/dist/server/assets/privacy-B6Wiez1P.js +93 -0
  134. package/dist/server/assets/product-links-CGYEPP56.js +16 -0
  135. package/dist/server/assets/product._id-BpRa-1z0.js +231 -0
  136. package/dist/server/assets/products.functions-DSlmibYN.js +209 -0
  137. package/dist/server/assets/products.functions-DlHkRiqi.js +24 -0
  138. package/dist/server/assets/profile-B0NWzVAZ.js +314 -0
  139. package/dist/server/assets/reset-password-CY-rmqMr.js +115 -0
  140. package/dist/server/assets/saved-7FA6Dbom.js +126 -0
  141. package/dist/server/assets/search-Yw5c_fZa.js +329 -0
  142. package/dist/server/assets/signup-UPzgZo4i.js +143 -0
  143. package/dist/server/assets/styles-DrNJG0BO.css +1 -0
  144. package/dist/server/assets/terms-CMnX95bP.js +89 -0
  145. package/dist/server/assets/update-password-Cr94ea8n.js +131 -0
  146. package/dist/server/assets/vendor-aria-hidden-DPa16MWu.js +122 -0
  147. package/dist/server/assets/vendor-class-variance-authority-0YxJPB9Y.js +44 -0
  148. package/dist/server/assets/vendor-cloudflare-unenv-preset-ya0VEFBz.js +250 -0
  149. package/dist/server/assets/vendor-clsx-DgYk2OaC.js +16 -0
  150. package/dist/server/assets/vendor-cookie-es-DAoofYiI.js +44 -0
  151. package/dist/server/assets/vendor-detect-node-es-l0sNRNKZ.js +1 -0
  152. package/dist/server/assets/vendor-floating-ui-core-3tkK0THV.js +726 -0
  153. package/dist/server/assets/vendor-floating-ui-dom-C-cPtgJv.js +626 -0
  154. package/dist/server/assets/vendor-floating-ui-react-dom-CRG6gBpH.js +319 -0
  155. package/dist/server/assets/vendor-floating-ui-utils-DmXANH-E.js +320 -0
  156. package/dist/server/assets/vendor-framer-motion-X4zAkX3J.js +1979 -0
  157. package/dist/server/assets/vendor-get-nonce-DiSj3EHl.js +9 -0
  158. package/dist/server/assets/vendor-h3-v2-CCobnLY5.js +287 -0
  159. package/dist/server/assets/vendor-iceberg-js-bHCkXyJn.js +534 -0
  160. package/dist/server/assets/vendor-isbot-CZ7WjwVs.js +21 -0
  161. package/dist/server/assets/vendor-lovable.dev-cloud-auth-js-BE03njZw.js +180 -0
  162. package/dist/server/assets/vendor-lucide-react-Ddew6HYb.js +458 -0
  163. package/dist/server/assets/vendor-motion-dom-D2MTwGIG.js +5983 -0
  164. package/dist/server/assets/vendor-motion-utils-LJlIFN6m.js +161 -0
  165. package/dist/server/assets/vendor-radix-ui-primitive-B-mNdDrH.js +11 -0
  166. package/dist/server/assets/vendor-radix-ui-react-accordion-1Izf6x00.js +308 -0
  167. package/dist/server/assets/vendor-radix-ui-react-arrow-B882lnFK.js +23 -0
  168. package/dist/server/assets/vendor-radix-ui-react-avatar-BVgZt2Ab.js +209 -0
  169. package/dist/server/assets/vendor-radix-ui-react-collapsible-DCBbMZiS.js +147 -0
  170. package/dist/server/assets/vendor-radix-ui-react-collection-BZ2srfgU.js +150 -0
  171. package/dist/server/assets/vendor-radix-ui-react-compose-refs-D3qsKVk1.js +39 -0
  172. package/dist/server/assets/vendor-radix-ui-react-context-BVoNDLue.js +78 -0
  173. package/dist/server/assets/vendor-radix-ui-react-dialog-DlxMaNYK.js +406 -0
  174. package/dist/server/assets/vendor-radix-ui-react-direction-Dt_WDL1t.js +9 -0
  175. package/dist/server/assets/vendor-radix-ui-react-dismissable-layer-CjsuPohV.js +210 -0
  176. package/dist/server/assets/vendor-radix-ui-react-dropdown-menu-DVxKumY8.js +263 -0
  177. package/dist/server/assets/vendor-radix-ui-react-focus-guards-D_6NoePE.js +29 -0
  178. package/dist/server/assets/vendor-radix-ui-react-focus-scope-DEIhTJJH.js +206 -0
  179. package/dist/server/assets/vendor-radix-ui-react-id-DFFpgh6m.js +14 -0
  180. package/dist/server/assets/vendor-radix-ui-react-menu-CiTMLwjT.js +893 -0
  181. package/dist/server/assets/vendor-radix-ui-react-popper-23Ye2Vyc.js +286 -0
  182. package/dist/server/assets/vendor-radix-ui-react-portal-CZCH5uPk.js +16 -0
  183. package/dist/server/assets/vendor-radix-ui-react-presence-CaAULlDU.js +128 -0
  184. package/dist/server/assets/vendor-radix-ui-react-primitive-BeOk3UYa.js +124 -0
  185. package/dist/server/assets/vendor-radix-ui-react-roving-focus-DES9GR8l.js +224 -0
  186. package/dist/server/assets/vendor-radix-ui-react-slot-DUhZbzoH.js +103 -0
  187. package/dist/server/assets/vendor-radix-ui-react-use-callback-ref-BynBgohw.js +11 -0
  188. package/dist/server/assets/vendor-radix-ui-react-use-controllable-state-C9KpT6DG.js +69 -0
  189. package/dist/server/assets/vendor-radix-ui-react-use-effect-event-gpNY2xjS.js +1 -0
  190. package/dist/server/assets/vendor-radix-ui-react-use-escape-keydown-CcYRQ2pp.js +17 -0
  191. package/dist/server/assets/vendor-radix-ui-react-use-is-hydrated-D_LcBPXY.js +15 -0
  192. package/dist/server/assets/vendor-radix-ui-react-use-layout-effect-1LNLXAjr.js +6 -0
  193. package/dist/server/assets/vendor-radix-ui-react-use-size-D6fiKJQo.js +39 -0
  194. package/dist/server/assets/vendor-react-DvBrY0qp.js +511 -0
  195. package/dist/server/assets/vendor-react-dom-yvMLPM0j.js +10484 -0
  196. package/dist/server/assets/vendor-react-remove-scroll-BNtiEvVN.js +328 -0
  197. package/dist/server/assets/vendor-react-remove-scroll-bar-hLqRASRk.js +82 -0
  198. package/dist/server/assets/vendor-react-style-singleton-BXjcXskB.js +69 -0
  199. package/dist/server/assets/vendor-rou3-3NaGPdI8.js +8 -0
  200. package/dist/server/assets/vendor-seroval-dJyC-Zhz.js +1775 -0
  201. package/dist/server/assets/vendor-seroval-plugins-Pq_U2meB.js +58 -0
  202. package/dist/server/assets/vendor-sonner-CqbjhsRh.js +1086 -0
  203. package/dist/server/assets/vendor-srvx-BA-baEX9.js +6 -0
  204. package/dist/server/assets/vendor-supabase-auth-js-D4xjVprw.js +7602 -0
  205. package/dist/server/assets/vendor-supabase-functions-js-sWy4UYn1.js +322 -0
  206. package/dist/server/assets/vendor-supabase-phoenix-Bw3Uh2Nn.js +1777 -0
  207. package/dist/server/assets/vendor-supabase-postgrest-js-AO-BXa7I.js +4938 -0
  208. package/dist/server/assets/vendor-supabase-realtime-js-BtdNgJbm.js +2111 -0
  209. package/dist/server/assets/vendor-supabase-storage-js-Dk_MrPYO.js +2679 -0
  210. package/dist/server/assets/vendor-supabase-supabase-js-D1EEtG3j.js +697 -0
  211. package/dist/server/assets/vendor-tailwind-merge-BHb_obmC.js +3255 -0
  212. package/dist/server/assets/vendor-tanstack-history-C4pKJmkt.js +204 -0
  213. package/dist/server/assets/vendor-tanstack-query-core-PwwTR5ld.js +2552 -0
  214. package/dist/server/assets/vendor-tanstack-react-query-hhHzXAK1.js +190 -0
  215. package/dist/server/assets/vendor-tanstack-react-router-XzqpA65A.js +1120 -0
  216. package/dist/server/assets/vendor-tanstack-react-start-RvWUpvat.js +37 -0
  217. package/dist/server/assets/vendor-tanstack-react-start-client-gpNY2xjS.js +1 -0
  218. package/dist/server/assets/vendor-tanstack-react-start-server-uj_Y9pEN.js +15 -0
  219. package/dist/server/assets/vendor-tanstack-react-store-gpNY2xjS.js +1 -0
  220. package/dist/server/assets/vendor-tanstack-router-core-6wywV3KN.js +4252 -0
  221. package/dist/server/assets/vendor-tanstack-start-client-core-DoOKV2pA.js +1741 -0
  222. package/dist/server/assets/vendor-tanstack-start-fn-stubs-l0sNRNKZ.js +1 -0
  223. package/dist/server/assets/vendor-tanstack-start-server-core-CsAstXv7.js +1421 -0
  224. package/dist/server/assets/vendor-tanstack-start-storage-context-DgH9hIJT.js +17 -0
  225. package/dist/server/assets/vendor-tanstack-store-l0sNRNKZ.js +1 -0
  226. package/dist/server/assets/vendor-tslib-_8ICaZ64.js +67 -0
  227. package/dist/server/assets/vendor-unenv-DUvF4YIF.js +544 -0
  228. package/dist/server/assets/vendor-use-callback-ref-DMFDRvmi.js +66 -0
  229. package/dist/server/assets/vendor-use-sidecar-DG1tHua4.js +106 -0
  230. package/dist/server/assets/vendor-use-sync-external-store-rZ8vi0It.js +64 -0
  231. package/dist/server/assets/vendor-vercel-analytics-oP8BDp0L.js +168 -0
  232. package/dist/server/assets/vendor-zod-BRyQdbC-.js +3580 -0
  233. package/dist/server/index.js +158 -0
  234. package/dist/server/wrangler.json +1 -0
  235. package/enable-powershell.ps1 +7 -0
  236. package/eslint.config.js +41 -0
  237. package/lint.bat +4 -0
  238. package/package.json +95 -0
  239. package/public/robots.txt +2 -0
  240. package/run-npm-build.cjs +20 -0
  241. package/run-npm-build.js +20 -0
  242. package/src/assets/hero-bg.jpg +0 -0
  243. package/src/assets/logo.png +0 -0
  244. package/src/components/scrollsy/Footer.tsx +68 -0
  245. package/src/components/scrollsy/LiveTicker.tsx +31 -0
  246. package/src/components/scrollsy/Logo.tsx +28 -0
  247. package/src/components/scrollsy/Nav.tsx +255 -0
  248. package/src/components/scrollsy/ProductCard.tsx +190 -0
  249. package/src/components/scrollsy/ProductFilters.tsx +226 -0
  250. package/src/components/scrollsy/SupportWidget.tsx +197 -0
  251. package/src/components/ui/accordion.tsx +51 -0
  252. package/src/components/ui/alert-dialog.tsx +115 -0
  253. package/src/components/ui/alert.tsx +49 -0
  254. package/src/components/ui/aspect-ratio.tsx +5 -0
  255. package/src/components/ui/avatar.tsx +47 -0
  256. package/src/components/ui/badge.tsx +32 -0
  257. package/src/components/ui/breadcrumb.tsx +101 -0
  258. package/src/components/ui/button.tsx +49 -0
  259. package/src/components/ui/calendar.tsx +177 -0
  260. package/src/components/ui/card.tsx +55 -0
  261. package/src/components/ui/carousel.tsx +240 -0
  262. package/src/components/ui/chart.tsx +331 -0
  263. package/src/components/ui/checkbox.tsx +26 -0
  264. package/src/components/ui/collapsible.tsx +11 -0
  265. package/src/components/ui/command.tsx +143 -0
  266. package/src/components/ui/context-menu.tsx +187 -0
  267. package/src/components/ui/dialog.tsx +104 -0
  268. package/src/components/ui/drawer.tsx +98 -0
  269. package/src/components/ui/dropdown-menu.tsx +188 -0
  270. package/src/components/ui/form.tsx +171 -0
  271. package/src/components/ui/hover-card.tsx +27 -0
  272. package/src/components/ui/input-otp.tsx +69 -0
  273. package/src/components/ui/input.tsx +22 -0
  274. package/src/components/ui/label.tsx +21 -0
  275. package/src/components/ui/menubar.tsx +229 -0
  276. package/src/components/ui/navigation-menu.tsx +120 -0
  277. package/src/components/ui/pagination.tsx +98 -0
  278. package/src/components/ui/popover.tsx +31 -0
  279. package/src/components/ui/progress.tsx +25 -0
  280. package/src/components/ui/radio-group.tsx +36 -0
  281. package/src/components/ui/resizable.tsx +37 -0
  282. package/src/components/ui/scroll-area.tsx +44 -0
  283. package/src/components/ui/select.tsx +152 -0
  284. package/src/components/ui/separator.tsx +24 -0
  285. package/src/components/ui/sheet.tsx +122 -0
  286. package/src/components/ui/sidebar.tsx +744 -0
  287. package/src/components/ui/skeleton.tsx +7 -0
  288. package/src/components/ui/slider.tsx +23 -0
  289. package/src/components/ui/sonner.tsx +23 -0
  290. package/src/components/ui/switch.tsx +27 -0
  291. package/src/components/ui/table.tsx +94 -0
  292. package/src/components/ui/tabs.tsx +53 -0
  293. package/src/components/ui/textarea.tsx +21 -0
  294. package/src/components/ui/toggle-group.tsx +57 -0
  295. package/src/components/ui/toggle.tsx +42 -0
  296. package/src/components/ui/tooltip.tsx +32 -0
  297. package/src/hooks/use-auth.ts +26 -0
  298. package/src/hooks/use-mobile.tsx +19 -0
  299. package/src/integrations/lovable/index.ts +41 -0
  300. package/src/lib/admin.functions.ts +564 -0
  301. package/src/lib/error-capture.ts +27 -0
  302. package/src/lib/error-page.ts +30 -0
  303. package/src/lib/product-links.ts +39 -0
  304. package/src/lib/products.functions.ts +101 -0
  305. package/src/lib/utils.ts +6 -0
  306. package/src/routeTree.gen.ts +480 -0
  307. package/src/router.tsx +16 -0
  308. package/src/routes/__root.tsx +177 -0
  309. package/src/routes/about.tsx +66 -0
  310. package/src/routes/admin.login.tsx +95 -0
  311. package/src/routes/admin.tsx +811 -0
  312. package/src/routes/affiliate-disclosure.tsx +35 -0
  313. package/src/routes/categories.tsx +57 -0
  314. package/src/routes/category.$slug.tsx +51 -0
  315. package/src/routes/contact.tsx +69 -0
  316. package/src/routes/faq.tsx +63 -0
  317. package/src/routes/index.tsx +269 -0
  318. package/src/routes/login.tsx +160 -0
  319. package/src/routes/privacy.tsx +39 -0
  320. package/src/routes/product.$id.tsx +212 -0
  321. package/src/routes/profile.tsx +393 -0
  322. package/src/routes/reset-password.tsx +71 -0
  323. package/src/routes/saved.tsx +83 -0
  324. package/src/routes/search.tsx +136 -0
  325. package/src/routes/signup.tsx +108 -0
  326. package/src/routes/sitemap[.]xml.ts +34 -0
  327. package/src/routes/terms.tsx +40 -0
  328. package/src/routes/update-password.tsx +91 -0
  329. package/src/server.ts +80 -0
  330. package/src/start.ts +24 -0
  331. package/src/styles.css +333 -0
  332. package/terminal-test-output.txt +1 -0
  333. package/tsconfig.json +27 -0
  334. package/vercel.json +26 -0
  335. package/vite.config.ts +38 -0
  336. package/wrangler.jsonc +7 -0
@@ -0,0 +1,255 @@
1
+ import { Link, useRouterState } from "@tanstack/react-router";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import {
4
+ Search,
5
+ Heart,
6
+ User as UserIcon,
7
+ Menu,
8
+ X,
9
+ Sparkles,
10
+ ChevronDown,
11
+ Shield,
12
+ LogOut,
13
+ } from "lucide-react";
14
+ import { useState } from "react";
15
+ import { Logo } from "./Logo";
16
+ import { useAuth } from "@/hooks/use-auth";
17
+ import { supabase } from "@/integrations/supabase/client";
18
+ import { Button } from "@/components/ui/button";
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuLabel,
24
+ DropdownMenuSeparator,
25
+ DropdownMenuTrigger,
26
+ } from "@/components/ui/dropdown-menu";
27
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
28
+
29
+ const navLinks = [
30
+ { to: "/", label: "Discover" },
31
+ { to: "/categories", label: "Categories" },
32
+ { to: "/search", label: "Search" },
33
+ ];
34
+
35
+ export function Nav() {
36
+ const { user } = useAuth();
37
+ const path = useRouterState({ select: (r) => r.location.pathname });
38
+ const [open, setOpen] = useState(false);
39
+
40
+ const { data: profile } = useQuery({
41
+ queryKey: ["nav-profile", user?.id],
42
+ enabled: !!user,
43
+ queryFn: async () => {
44
+ const { data } = await supabase
45
+ .from("profiles")
46
+ .select("display_name, avatar_url")
47
+ .eq("id", user!.id)
48
+ .maybeSingle();
49
+ return data ?? null;
50
+ },
51
+ });
52
+
53
+ const initials =
54
+ profile?.display_name?.slice(0, 1)?.toUpperCase() ??
55
+ user?.email?.slice(0, 1)?.toUpperCase() ??
56
+ "U";
57
+
58
+ return (
59
+ <header className="sticky top-0 z-50 px-3 pt-3">
60
+ <nav className="glass-strong rounded-2xl px-4 md:px-6 h-14 flex items-center justify-between max-w-7xl mx-auto shadow-card">
61
+ <div className="flex items-center gap-8">
62
+ <Logo />
63
+ <div className="hidden md:flex items-center gap-1">
64
+ {navLinks.map((l) => (
65
+ <Link
66
+ key={l.to}
67
+ to={l.to}
68
+ className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
69
+ path === l.to
70
+ ? "text-foreground bg-white/5"
71
+ : "text-muted-foreground hover:text-foreground hover:bg-white/5"
72
+ }`}
73
+ >
74
+ {l.label}
75
+ </Link>
76
+ ))}
77
+ </div>
78
+ </div>
79
+
80
+ <div className="flex items-center gap-2">
81
+ <Link
82
+ to="/search"
83
+ search={{ q: "" }}
84
+ className="hidden sm:flex items-center gap-2 glass rounded-full px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition w-56"
85
+ >
86
+ <Search className="w-4 h-4" />
87
+ <span>Search trending finds…</span>
88
+ </Link>
89
+
90
+ {user ? (
91
+ <>
92
+ <Link
93
+ to="/saved"
94
+ className="hidden sm:inline-flex p-2 rounded-full hover:bg-white/5 transition"
95
+ aria-label="Saved"
96
+ >
97
+ <Heart className="w-5 h-5" />
98
+ </Link>
99
+
100
+ <DropdownMenu>
101
+ <DropdownMenuTrigger asChild>
102
+ <button
103
+ className="flex items-center gap-2 rounded-full p-1.5 pr-2.5 hover:bg-white/5 transition"
104
+ aria-label="Open profile menu"
105
+ >
106
+ <Avatar className="h-9 w-9 border border-white/10">
107
+ <AvatarImage
108
+ src={profile?.avatar_url ?? undefined}
109
+ alt={profile?.display_name ?? "Profile"}
110
+ />
111
+ <AvatarFallback className="bg-brand-magenta/20 text-sm font-semibold text-foreground">
112
+ {initials}
113
+ </AvatarFallback>
114
+ </Avatar>
115
+ <ChevronDown className="hidden sm:block w-4 h-4 text-muted-foreground" />
116
+ </button>
117
+ </DropdownMenuTrigger>
118
+
119
+ <DropdownMenuContent align="end" className="w-64">
120
+ <DropdownMenuLabel>
121
+ <div className="flex items-center gap-3">
122
+ <Avatar className="h-10 w-10 border border-white/10">
123
+ <AvatarImage
124
+ src={profile?.avatar_url ?? undefined}
125
+ alt={profile?.display_name ?? "Profile"}
126
+ />
127
+ <AvatarFallback className="bg-brand-magenta/20 text-sm font-semibold text-foreground">
128
+ {initials}
129
+ </AvatarFallback>
130
+ </Avatar>
131
+ <div className="min-w-0">
132
+ <div className="truncate text-sm font-semibold">
133
+ {profile?.display_name || "Your account"}
134
+ </div>
135
+ <div className="truncate text-xs text-muted-foreground">{user.email}</div>
136
+ </div>
137
+ </div>
138
+ </DropdownMenuLabel>
139
+
140
+ <DropdownMenuSeparator />
141
+
142
+ <DropdownMenuItem asChild>
143
+ <Link to="/profile" className="flex w-full items-center gap-2">
144
+ <UserIcon className="w-4 h-4" />
145
+ Profile
146
+ </Link>
147
+ </DropdownMenuItem>
148
+
149
+ <DropdownMenuItem asChild>
150
+ <Link to="/saved" className="flex w-full items-center gap-2">
151
+ <Heart className="w-4 h-4" />
152
+ Saved items
153
+ </Link>
154
+ </DropdownMenuItem>
155
+
156
+ <DropdownMenuItem asChild>
157
+ <Link to="/admin" className="flex w-full items-center gap-2">
158
+ <Shield className="w-4 h-4" />
159
+ Admin page
160
+ </Link>
161
+ </DropdownMenuItem>
162
+
163
+ <DropdownMenuSeparator />
164
+
165
+ <DropdownMenuItem
166
+ className="flex items-center gap-2 text-destructive focus:text-destructive"
167
+ onSelect={() => {
168
+ supabase.auth.signOut();
169
+ }}
170
+ >
171
+ <LogOut className="w-4 h-4" />
172
+ Sign out
173
+ </DropdownMenuItem>
174
+ </DropdownMenuContent>
175
+ </DropdownMenu>
176
+ </>
177
+ ) : (
178
+ <>
179
+ <Link
180
+ to="/login"
181
+ className="hidden sm:inline-flex px-3 py-1.5 text-sm rounded-lg hover:bg-white/5 transition"
182
+ >
183
+ Log in
184
+ </Link>
185
+ <Link
186
+ to="/signup"
187
+ className="btn-glow rounded-full px-4 py-1.5 text-sm font-semibold inline-flex items-center gap-1.5"
188
+ >
189
+ <Sparkles className="w-3.5 h-3.5" /> Join free
190
+ </Link>
191
+ </>
192
+ )}
193
+
194
+ <button
195
+ className="md:hidden p-2 rounded-lg hover:bg-white/5"
196
+ onClick={() => setOpen(!open)}
197
+ aria-label="Menu"
198
+ >
199
+ {open ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
200
+ </button>
201
+ </div>
202
+ </nav>
203
+
204
+ {open && (
205
+ <div className="md:hidden glass-strong rounded-2xl mt-2 max-w-7xl mx-auto p-3 animate-fade-up">
206
+ {navLinks.map((l) => (
207
+ <Link
208
+ key={l.to}
209
+ to={l.to}
210
+ onClick={() => setOpen(false)}
211
+ className="block px-3 py-2 rounded-lg text-sm hover:bg-white/5"
212
+ >
213
+ {l.label}
214
+ </Link>
215
+ ))}
216
+
217
+ {user ? (
218
+ <div className="mt-2 border-t border-white/10 pt-2 space-y-1">
219
+ <Link
220
+ to="/profile"
221
+ onClick={() => setOpen(false)}
222
+ className="block px-3 py-2 rounded-lg text-sm hover:bg-white/5"
223
+ >
224
+ Profile
225
+ </Link>
226
+ <Link
227
+ to="/saved"
228
+ onClick={() => setOpen(false)}
229
+ className="block px-3 py-2 rounded-lg text-sm hover:bg-white/5"
230
+ >
231
+ Saved items
232
+ </Link>
233
+ <Link
234
+ to="/admin"
235
+ onClick={() => setOpen(false)}
236
+ className="block px-3 py-2 rounded-lg text-sm hover:bg-white/5"
237
+ >
238
+ Admin page
239
+ </Link>
240
+ <button
241
+ onClick={async () => {
242
+ setOpen(false);
243
+ await supabase.auth.signOut();
244
+ }}
245
+ className="w-full text-left px-3 py-2 rounded-lg text-sm text-destructive hover:bg-white/5"
246
+ >
247
+ Sign out
248
+ </button>
249
+ </div>
250
+ ) : null}
251
+ </div>
252
+ )}
253
+ </header>
254
+ );
255
+ }
@@ -0,0 +1,190 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { Heart, ExternalLink, Flame, TrendingUp, Truck } from "lucide-react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useState } from "react";
5
+ import { supabase } from "@/integrations/supabase/client";
6
+ import { useAuth } from "@/hooks/use-auth";
7
+ import { toast } from "sonner";
8
+ import { cn } from "@/lib/utils";
9
+ import { resolveBuyUrl, resolveProductLabel } from "@/lib/product-links";
10
+
11
+ export type Product = {
12
+ id: string;
13
+ title: string;
14
+ description: string | null;
15
+ image_url: string;
16
+ price: number;
17
+ original_price: number | null;
18
+ affiliate_url: string;
19
+ merchant: string | null;
20
+ badges: string[];
21
+ tags: string[];
22
+ is_trending: boolean;
23
+ is_hot: boolean;
24
+ shipping_info: string | null;
25
+ delivery_estimate: string | null;
26
+ view_count: number;
27
+ };
28
+
29
+ export function ProductCard({
30
+ product,
31
+ aspect = "auto",
32
+ className,
33
+ priority = false,
34
+ }: {
35
+ product: Product;
36
+ aspect?: "auto" | "square" | "tall" | "wide";
37
+ className?: string;
38
+ priority?: boolean;
39
+ }) {
40
+ const { user } = useAuth();
41
+ const qc = useQueryClient();
42
+
43
+ const { data: savedIds } = useQuery({
44
+ queryKey: ["saved-ids", user?.id],
45
+ enabled: !!user,
46
+ queryFn: async () => {
47
+ const { data } = await supabase
48
+ .from("saved_items")
49
+ .select("product_id")
50
+ .eq("user_id", user!.id);
51
+ return new Set((data ?? []).map((r) => r.product_id));
52
+ },
53
+ });
54
+ const isSaved = savedIds?.has(product.id) ?? false;
55
+
56
+ const toggleSave = useMutation({
57
+ mutationFn: async () => {
58
+ if (!user) throw new Error("login");
59
+ if (isSaved) {
60
+ await supabase
61
+ .from("saved_items")
62
+ .delete()
63
+ .eq("user_id", user.id)
64
+ .eq("product_id", product.id);
65
+ } else {
66
+ await supabase.from("saved_items").insert({ user_id: user.id, product_id: product.id });
67
+ }
68
+ },
69
+ onSuccess: () => {
70
+ qc.invalidateQueries({ queryKey: ["saved-ids", user?.id] });
71
+ qc.invalidateQueries({ queryKey: ["saved-products", user?.id] });
72
+ toast.success(isSaved ? "Removed from saved" : "Saved to favorites", { duration: 1500 });
73
+ },
74
+ onError: (e: Error) => {
75
+ if (e.message === "login") toast.error("Sign in to save items");
76
+ },
77
+ });
78
+
79
+ const discount = product.original_price
80
+ ? Math.round((1 - Number(product.price) / Number(product.original_price)) * 100)
81
+ : 0;
82
+
83
+ const aspectClass = {
84
+ auto: "",
85
+ square: "aspect-square",
86
+ tall: "aspect-[3/4]",
87
+ wide: "aspect-[4/3]",
88
+ }[aspect];
89
+
90
+ const buyUrl = resolveBuyUrl(product);
91
+ const productLabel = resolveProductLabel(product);
92
+ const [imageError, setImageError] = useState(false);
93
+
94
+ return (
95
+ <article
96
+ className={cn(
97
+ "group card-tilt relative overflow-hidden rounded-3xl bg-gradient-card border border-white/5",
98
+ className,
99
+ )}
100
+ >
101
+ <Link to="/product/$id" params={{ id: product.id }} className="block">
102
+ <div className={cn("relative overflow-hidden", aspectClass)}>
103
+ <img
104
+ src={
105
+ imageError
106
+ ? "https://images.unsplash.com/photo-1556742205-9fe6f32a8f6f?auto=format&fit=crop&w=1200&q=80"
107
+ : product.image_url
108
+ }
109
+ alt={product.title}
110
+ loading={priority ? "eager" : "lazy"}
111
+ onError={() => setImageError(true)}
112
+ className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
113
+ />
114
+ <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/0 to-black/0" />
115
+
116
+ {/* Top-left badges */}
117
+ <div className="absolute top-3 left-3 flex flex-col gap-1.5">
118
+ {discount > 0 && (
119
+ <span className="bg-gradient-brand text-white text-[11px] font-bold px-2 py-1 rounded-full shadow-lg">
120
+ -{discount}%
121
+ </span>
122
+ )}
123
+ {product.is_hot && (
124
+ <span className="glass text-[10px] font-semibold px-2 py-1 rounded-full inline-flex items-center gap-1">
125
+ <Flame className="w-3 h-3 text-brand-orange" /> Hot
126
+ </span>
127
+ )}
128
+ {product.is_trending && !product.is_hot && (
129
+ <span className="glass text-[10px] font-semibold px-2 py-1 rounded-full inline-flex items-center gap-1">
130
+ <TrendingUp className="w-3 h-3 text-brand-magenta" /> Trending
131
+ </span>
132
+ )}
133
+ </div>
134
+ </div>
135
+
136
+ <div className="p-4 space-y-2">
137
+ <h3 className="font-display font-semibold text-sm leading-snug line-clamp-2 min-h-[2.5rem]">
138
+ {product.title}
139
+ </h3>
140
+ {product.description && (
141
+ <p className="text-xs text-muted-foreground line-clamp-2">{product.description}</p>
142
+ )}
143
+ <div className="flex items-baseline gap-2 pt-1">
144
+ <span className="text-lg font-bold text-gradient">
145
+ ${Number(product.price).toFixed(2)}
146
+ </span>
147
+ {product.original_price && (
148
+ <span className="text-xs text-muted-foreground line-through">
149
+ ${Number(product.original_price).toFixed(2)}
150
+ </span>
151
+ )}
152
+ </div>
153
+ {product.shipping_info && (
154
+ <div className="flex items-center gap-1 text-[11px] text-muted-foreground">
155
+ <Truck className="w-3 h-3" />
156
+ <span>{product.shipping_info}</span>
157
+ {product.delivery_estimate && <span>· {product.delivery_estimate}</span>}
158
+ </div>
159
+ )}
160
+ </div>
161
+ </Link>
162
+
163
+ {/* Floating action buttons */}
164
+ <button
165
+ onClick={(e) => {
166
+ e.preventDefault();
167
+ toggleSave.mutate();
168
+ }}
169
+ className={cn(
170
+ "absolute top-3 right-3 p-2 rounded-full glass transition-all",
171
+ isSaved && "bg-gradient-brand border-transparent",
172
+ )}
173
+ aria-label={isSaved ? "Unsave" : "Save"}
174
+ >
175
+ <Heart className={cn("w-4 h-4", isSaved ? "fill-white text-white" : "text-white/90")} />
176
+ </button>
177
+
178
+ <a
179
+ href={buyUrl ?? product.affiliate_url}
180
+ target="_blank"
181
+ rel="noopener noreferrer"
182
+ onClick={(e) => e.stopPropagation()}
183
+ className="absolute bottom-4 right-4 opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all btn-glow rounded-full px-3 py-1.5 text-xs font-semibold inline-flex items-center gap-1"
184
+ title={`Open ${productLabel}`}
185
+ >
186
+ Buy Now <ExternalLink className="w-3 h-3" />
187
+ </a>
188
+ </article>
189
+ );
190
+ }
@@ -0,0 +1,226 @@
1
+ import type { ReactNode } from "react";
2
+ import { Filter, RotateCcw, SlidersHorizontal } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export type ProductSort = "relevance" | "newest" | "popular" | "price-asc" | "price-desc";
6
+
7
+ export type ProductFiltersState = {
8
+ sort: ProductSort;
9
+ maxPrice: string;
10
+ trending: boolean;
11
+ hot: boolean;
12
+ featured: boolean;
13
+ freeShipping: boolean;
14
+ };
15
+
16
+ export type FilterableProduct = {
17
+ id: string;
18
+ title: string;
19
+ price: number;
20
+ original_price: number | null;
21
+ view_count: number;
22
+ created_at?: string;
23
+ is_trending?: boolean;
24
+ is_hot?: boolean;
25
+ is_featured?: boolean;
26
+ shipping_info?: string | null;
27
+ };
28
+
29
+ export const defaultProductFilters = (): ProductFiltersState => ({
30
+ sort: "relevance",
31
+ maxPrice: "",
32
+ trending: false,
33
+ hot: false,
34
+ featured: false,
35
+ freeShipping: false,
36
+ });
37
+
38
+ export function applyProductFilters(products: FilterableProduct[], filters: ProductFiltersState) {
39
+ const maxPrice = filters.maxPrice ? Number(filters.maxPrice) : Number.POSITIVE_INFINITY;
40
+
41
+ let filtered = products.filter((product) => {
42
+ if (Number.isFinite(maxPrice) && product.price > maxPrice) return false;
43
+ if (filters.trending && !product.is_trending) return false;
44
+ if (filters.hot && !product.is_hot) return false;
45
+ if (filters.featured && !product.is_featured) return false;
46
+ if (filters.freeShipping && !product.shipping_info?.toLowerCase().includes("free"))
47
+ return false;
48
+ return true;
49
+ });
50
+
51
+ filtered = [...filtered].sort((a, b) => {
52
+ switch (filters.sort) {
53
+ case "newest":
54
+ return new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime();
55
+ case "popular":
56
+ return (b.view_count ?? 0) - (a.view_count ?? 0);
57
+ case "price-asc":
58
+ return a.price - b.price;
59
+ case "price-desc":
60
+ return b.price - a.price;
61
+ default:
62
+ return 0;
63
+ }
64
+ });
65
+
66
+ return filtered;
67
+ }
68
+
69
+ export function ProductFilters({
70
+ filters,
71
+ onChange,
72
+ resultCount,
73
+ }: {
74
+ filters: ProductFiltersState;
75
+ onChange: (next: ProductFiltersState) => void;
76
+ resultCount: number;
77
+ }) {
78
+ const set = <K extends keyof ProductFiltersState>(key: K, value: ProductFiltersState[K]) =>
79
+ onChange({ ...filters, [key]: value });
80
+
81
+ const reset = () => onChange(defaultProductFilters());
82
+
83
+ return (
84
+ <div className="glass-strong rounded-3xl p-4 md:p-5 border border-white/10">
85
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
86
+ <div>
87
+ <div className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
88
+ <SlidersHorizontal className="w-4 h-4" />
89
+ Filters
90
+ </div>
91
+ <h2 className="font-display text-xl font-bold mt-2">Refine your results</h2>
92
+ <p className="text-sm text-muted-foreground mt-1">
93
+ {resultCount} products match your filters
94
+ </p>
95
+ </div>
96
+
97
+ <button
98
+ type="button"
99
+ onClick={reset}
100
+ className="inline-flex items-center justify-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm hover:bg-white/5 transition"
101
+ >
102
+ <RotateCcw className="w-4 h-4" />
103
+ Reset
104
+ </button>
105
+ </div>
106
+
107
+ <div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
108
+ <Field label="Sort by">
109
+ <select
110
+ value={filters.sort}
111
+ onChange={(e) => set("sort", e.target.value as ProductSort)}
112
+ className="w-full rounded-2xl border border-white/10 bg-background/60 px-3 py-2.5 text-sm outline-none"
113
+ >
114
+ <option value="relevance">Relevance</option>
115
+ <option value="newest">Newest</option>
116
+ <option value="popular">Most popular</option>
117
+ <option value="price-asc">Price: low to high</option>
118
+ <option value="price-desc">Price: high to low</option>
119
+ </select>
120
+ </Field>
121
+
122
+ <Field label="Max price">
123
+ <input
124
+ inputMode="numeric"
125
+ type="number"
126
+ min="0"
127
+ value={filters.maxPrice}
128
+ onChange={(e) => set("maxPrice", e.target.value)}
129
+ placeholder="Any price"
130
+ className="w-full rounded-2xl border border-white/10 bg-background/60 px-3 py-2.5 text-sm outline-none"
131
+ />
132
+ </Field>
133
+
134
+ <FilterToggle
135
+ label="Trending"
136
+ checked={filters.trending}
137
+ onChange={(checked) => set("trending", checked)}
138
+ />
139
+ <FilterToggle
140
+ label="Hot"
141
+ checked={filters.hot}
142
+ onChange={(checked) => set("hot", checked)}
143
+ />
144
+ <FilterToggle
145
+ label="Featured"
146
+ checked={filters.featured}
147
+ onChange={(checked) => set("featured", checked)}
148
+ />
149
+ </div>
150
+
151
+ <div className="mt-3 flex flex-wrap gap-2">
152
+ <FilterPill
153
+ active={filters.freeShipping}
154
+ onClick={() => set("freeShipping", !filters.freeShipping)}
155
+ label="Free shipping"
156
+ icon={<Filter className="w-3.5 h-3.5" />}
157
+ />
158
+ </div>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function Field({ label, children }: { label: string; children: ReactNode }) {
164
+ return (
165
+ <label className="block">
166
+ <span className="mb-2 block text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
167
+ {label}
168
+ </span>
169
+ {children}
170
+ </label>
171
+ );
172
+ }
173
+
174
+ function FilterToggle({
175
+ label,
176
+ checked,
177
+ onChange,
178
+ }: {
179
+ label: string;
180
+ checked: boolean;
181
+ onChange: (checked: boolean) => void;
182
+ }) {
183
+ return (
184
+ <button
185
+ type="button"
186
+ onClick={() => onChange(!checked)}
187
+ className={cn(
188
+ "rounded-2xl border px-4 py-3 text-left text-sm transition",
189
+ checked
190
+ ? "border-brand-magenta/60 bg-brand-magenta/15 text-foreground shadow-lg"
191
+ : "border-white/10 bg-background/40 text-muted-foreground hover:bg-white/5 hover:text-foreground",
192
+ )}
193
+ >
194
+ <span className="block font-medium">{label}</span>
195
+ <span className="mt-0.5 block text-xs opacity-80">{checked ? "On" : "Off"}</span>
196
+ </button>
197
+ );
198
+ }
199
+
200
+ function FilterPill({
201
+ label,
202
+ icon,
203
+ active,
204
+ onClick,
205
+ }: {
206
+ label: string;
207
+ icon: ReactNode;
208
+ active: boolean;
209
+ onClick: () => void;
210
+ }) {
211
+ return (
212
+ <button
213
+ type="button"
214
+ onClick={onClick}
215
+ className={cn(
216
+ "inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm transition",
217
+ active
218
+ ? "bg-foreground text-background"
219
+ : "border border-white/10 bg-background/40 text-muted-foreground hover:bg-white/5 hover:text-foreground",
220
+ )}
221
+ >
222
+ {icon}
223
+ {label}
224
+ </button>
225
+ );
226
+ }