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,393 @@
1
+ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
2
+ import { useAuth } from "@/hooks/use-auth";
3
+ import { supabase } from "@/integrations/supabase/client";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ ArrowRight,
8
+ Camera,
9
+ Crown,
10
+ Heart,
11
+ LogOut,
12
+ Shield,
13
+ ShoppingBag,
14
+ Star,
15
+ User as UserIcon,
16
+ } from "lucide-react";
17
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
18
+ import { useQuery } from "@tanstack/react-query";
19
+
20
+ export const Route = createFileRoute("/profile")({
21
+ head: () => ({ meta: [{ title: "Profile · Scrollsy" }] }),
22
+ component: Profile,
23
+ });
24
+
25
+ function Profile() {
26
+ const { user, loading } = useAuth();
27
+ const nav = useNavigate();
28
+ const [name, setName] = useState("");
29
+ const [avatarUrl, setAvatarUrl] = useState("");
30
+ const [saving, setSaving] = useState(false);
31
+ const [avatarSaving, setAvatarSaving] = useState(false);
32
+
33
+ const { data: roleData } = useQuery({
34
+ queryKey: ["profile-role", user?.id],
35
+ enabled: !!user,
36
+ queryFn: async () => {
37
+ const { data } = await supabase
38
+ .from("user_roles")
39
+ .select("admin_id")
40
+ .eq("user_id", user!.id)
41
+ .eq("role", "admin")
42
+ .maybeSingle();
43
+ return data ?? null;
44
+ },
45
+ });
46
+
47
+ useEffect(() => {
48
+ if (!user) return;
49
+ supabase
50
+ .from("profiles")
51
+ .select("display_name, avatar_url")
52
+ .eq("id", user.id)
53
+ .maybeSingle()
54
+ .then(({ data }) => {
55
+ setName(data?.display_name ?? "");
56
+ setAvatarUrl(data?.avatar_url ?? "");
57
+ });
58
+ }, [user]);
59
+
60
+ const initials = useMemo(() => {
61
+ return (
62
+ name?.trim()?.slice(0, 1)?.toUpperCase() ?? user?.email?.slice(0, 1)?.toUpperCase() ?? "U"
63
+ );
64
+ }, [name, user?.email]);
65
+
66
+ if (loading) return <div className="px-4 max-w-7xl mx-auto pt-10">Loading…</div>;
67
+
68
+ if (!user) {
69
+ return (
70
+ <div className="max-w-md mx-auto pt-24 px-4 text-center">
71
+ <h1 className="font-display text-2xl font-bold">Sign in to view your profile</h1>
72
+ <p className="mt-2 text-sm text-muted-foreground">
73
+ Track saved finds, update your account, and manage your shop profile.
74
+ </p>
75
+ <Link
76
+ to="/login"
77
+ className="btn-glow inline-block mt-6 rounded-full px-6 py-3 font-semibold text-sm"
78
+ >
79
+ Sign in
80
+ </Link>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ const saveProfile = async () => {
86
+ setSaving(true);
87
+ const { error } = await supabase.from("profiles").upsert({
88
+ id: user.id,
89
+ display_name: name,
90
+ avatar_url: avatarUrl || null,
91
+ updated_at: new Date().toISOString(),
92
+ });
93
+ setSaving(false);
94
+
95
+ if (error) return toast.error(error.message);
96
+ toast.success("Profile updated");
97
+ };
98
+
99
+ const onAvatarPick = async (file: File | null) => {
100
+ if (!file) return;
101
+
102
+ if (!file.type.startsWith("image/")) {
103
+ toast.error("Please choose an image file");
104
+ return;
105
+ }
106
+
107
+ setAvatarSaving(true);
108
+ try {
109
+ const dataUrl = await fileToDataUrl(file);
110
+ setAvatarUrl(dataUrl);
111
+ const { error } = await supabase.from("profiles").upsert({
112
+ id: user.id,
113
+ display_name: name,
114
+ avatar_url: dataUrl,
115
+ updated_at: new Date().toISOString(),
116
+ });
117
+ if (error) throw error;
118
+ toast.success("Profile photo updated");
119
+ } catch (error: any) {
120
+ toast.error(error?.message ?? "Could not update profile photo");
121
+ } finally {
122
+ setAvatarSaving(false);
123
+ }
124
+ };
125
+
126
+ return (
127
+ <div className="px-4 max-w-7xl mx-auto pt-10 pb-16">
128
+ <div className="grid gap-6 lg:grid-cols-[320px_1fr]">
129
+ <aside className="glass-strong rounded-[2rem] p-6 shadow-elevated h-fit">
130
+ <div className="relative">
131
+ <div className="absolute inset-x-0 top-0 h-32 rounded-[1.5rem] bg-gradient-brand opacity-90" />
132
+ <div className="relative pt-10 pb-6 px-4">
133
+ <div className="flex justify-center">
134
+ <div className="relative">
135
+ <Avatar className="h-28 w-28 border-[6px] border-background shadow-2xl">
136
+ <AvatarImage src={avatarUrl || undefined} alt={name || "Profile photo"} />
137
+ <AvatarFallback className="bg-brand-magenta/20 text-2xl font-bold text-foreground">
138
+ {initials}
139
+ </AvatarFallback>
140
+ </Avatar>
141
+
142
+ <label className="absolute bottom-1 right-1 inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-foreground text-background shadow-lg transition hover:scale-105">
143
+ <Camera className="h-4 w-4" />
144
+ <input
145
+ type="file"
146
+ accept="image/*"
147
+ className="hidden"
148
+ onChange={(e) => onAvatarPick(e.target.files?.[0] ?? null)}
149
+ disabled={avatarSaving}
150
+ />
151
+ </label>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="mt-5 text-center">
156
+ <h1 className="font-display text-2xl font-bold">{name || "Scrollster"}</h1>
157
+ <p className="text-sm text-muted-foreground break-all">{user.email}</p>
158
+
159
+ {roleData?.admin_id ? (
160
+ <div className="mt-3 inline-flex items-center gap-2 rounded-full border border-brand-amber/20 bg-brand-amber/10 px-3 py-1 text-xs font-semibold text-brand-amber">
161
+ {roleData.admin_id === "doraexplora" ? (
162
+ <Crown className="h-3.5 w-3.5" />
163
+ ) : (
164
+ <Shield className="h-3.5 w-3.5" />
165
+ )}
166
+ {roleData.admin_id === "doraexplora"
167
+ ? "Master Admin"
168
+ : `Admin ID ${roleData.admin_id}`}
169
+ </div>
170
+ ) : null}
171
+ </div>
172
+
173
+ <div className="mt-6 grid grid-cols-2 gap-3">
174
+ <QuickStat label="Saved items" value="24" icon={<Heart className="h-4 w-4" />} />
175
+ <QuickStat label="Reviews" value="8" icon={<Star className="h-4 w-4" />} />
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </aside>
180
+
181
+ <main className="space-y-6">
182
+ <section className="glass-strong rounded-[2rem] p-6 md:p-8 shadow-elevated">
183
+ <div className="flex flex-col gap-6 md:flex-row md:items-start md:justify-between">
184
+ <div className="max-w-2xl">
185
+ <div className="inline-flex items-center gap-2 rounded-full bg-brand-magenta/15 px-3 py-1 text-xs font-semibold text-brand-magenta">
186
+ <ShoppingBag className="h-3.5 w-3.5" />
187
+ Shopping profile
188
+ </div>
189
+ <h2 className="mt-3 font-display text-3xl md:text-4xl font-bold tracking-tight">
190
+ Your account, styled like a marketplace profile
191
+ </h2>
192
+ <p className="mt-3 text-sm md:text-base text-muted-foreground">
193
+ Update your display name and photo, access saved deals, and jump to admin tools if
194
+ you manage the store.
195
+ </p>
196
+
197
+ <div className="mt-6 flex flex-wrap gap-3">
198
+ <Link
199
+ to="/saved"
200
+ className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm hover:bg-white/5 transition"
201
+ >
202
+ <Heart className="h-4 w-4" />
203
+ Saved items
204
+ </Link>
205
+ <Link
206
+ to="/search"
207
+ search={{ q: "" }}
208
+ className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm hover:bg-white/5 transition"
209
+ >
210
+ <ArrowRight className="h-4 w-4" />
211
+ Keep shopping
212
+ </Link>
213
+ {roleData?.admin_id ? (
214
+ <Link
215
+ to="/admin"
216
+ className="inline-flex items-center gap-2 rounded-full border border-brand-magenta/20 bg-brand-magenta/10 px-4 py-2 text-sm font-medium text-foreground hover:bg-brand-magenta/15 transition"
217
+ >
218
+ <Shield className="h-4 w-4" />
219
+ Admin page
220
+ </Link>
221
+ ) : null}
222
+ </div>
223
+ </div>
224
+
225
+ <div className="grid gap-3 sm:grid-cols-3 md:w-[360px]">
226
+ <MiniCard title="Wishlist" value="12" detail="Items ready to buy" />
227
+ <MiniCard title="Orders" value="3" detail="Recent purchases" />
228
+ <MiniCard title="Rewards" value="480" detail="Points available" />
229
+ </div>
230
+ </div>
231
+ </section>
232
+
233
+ <section className="grid gap-6 md:grid-cols-2">
234
+ <div className="glass-strong rounded-[2rem] p-6 shadow-card">
235
+ <h3 className="font-display text-xl font-bold">Profile details</h3>
236
+ <div className="mt-5 space-y-4">
237
+ <label className="block">
238
+ <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
239
+ Display name
240
+ </span>
241
+ <input
242
+ value={name}
243
+ onChange={(e) => setName(e.target.value)}
244
+ className="w-full rounded-2xl border border-white/10 bg-background/60 px-4 py-3 text-sm outline-none"
245
+ placeholder="Your display name"
246
+ />
247
+ </label>
248
+
249
+ <label className="block">
250
+ <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
251
+ Profile photo
252
+ </span>
253
+ <div className="rounded-2xl border border-dashed border-white/10 bg-background/40 px-4 py-4 text-sm text-muted-foreground">
254
+ Upload a square image to make your profile look polished in the marketplace UI.
255
+ </div>
256
+ </label>
257
+ </div>
258
+
259
+ <div className="mt-6 flex flex-wrap gap-3">
260
+ <button
261
+ onClick={saveProfile}
262
+ disabled={saving}
263
+ className="btn-glow rounded-full px-5 py-2.5 text-sm font-semibold disabled:opacity-60"
264
+ >
265
+ {saving ? "Saving…" : "Save changes"}
266
+ </button>
267
+ <button
268
+ onClick={() => {
269
+ setName(user.user_metadata?.display_name ?? "");
270
+ setAvatarUrl("");
271
+ }}
272
+ className="rounded-full border border-white/10 px-5 py-2.5 text-sm hover:bg-white/5 transition"
273
+ >
274
+ Reset
275
+ </button>
276
+ </div>
277
+ </div>
278
+
279
+ <div className="glass-strong rounded-[2rem] p-6 shadow-card">
280
+ <h3 className="font-display text-xl font-bold">Quick links</h3>
281
+ <div className="mt-5 space-y-3">
282
+ <ActionLink
283
+ to="/saved"
284
+ icon={<Heart className="h-4 w-4" />}
285
+ title="Saved items"
286
+ description="Open your wishlist and compare favorites."
287
+ />
288
+ <ActionLink
289
+ to="/categories"
290
+ icon={<ShoppingBag className="h-4 w-4" />}
291
+ title="Browse categories"
292
+ description="Jump back into shopping by mood or category."
293
+ />
294
+ <ActionLink
295
+ to="/admin"
296
+ icon={<Shield className="h-4 w-4" />}
297
+ title="Admin page"
298
+ description="Manage products and admins."
299
+ hidden={!roleData?.admin_id}
300
+ />
301
+ </div>
302
+
303
+ <div className="mt-6 flex items-center justify-between rounded-2xl border border-white/10 bg-background/40 px-4 py-3">
304
+ <div>
305
+ <div className="text-sm font-medium">Sign out</div>
306
+ <div className="text-xs text-muted-foreground">End your current session</div>
307
+ </div>
308
+ <button
309
+ onClick={async () => {
310
+ await supabase.auth.signOut();
311
+ nav({ to: "/" });
312
+ }}
313
+ className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm hover:bg-white/5 transition"
314
+ >
315
+ <LogOut className="h-4 w-4" />
316
+ Sign out
317
+ </button>
318
+ </div>
319
+ </div>
320
+ </section>
321
+ </main>
322
+ </div>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ function fileToDataUrl(file: File) {
328
+ return new Promise<string>((resolve, reject) => {
329
+ const reader = new FileReader();
330
+ reader.onload = () => resolve(String(reader.result ?? ""));
331
+ reader.onerror = () => reject(new Error("Failed to read image"));
332
+ reader.readAsDataURL(file);
333
+ });
334
+ }
335
+
336
+ function QuickStat({
337
+ label,
338
+ value,
339
+ icon,
340
+ }: {
341
+ label: string;
342
+ value: string;
343
+ icon: React.ReactNode;
344
+ }) {
345
+ return (
346
+ <div className="rounded-2xl border border-white/10 bg-background/40 p-4">
347
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
348
+ {icon}
349
+ {label}
350
+ </div>
351
+ <div className="mt-1 font-display text-2xl font-bold">{value}</div>
352
+ </div>
353
+ );
354
+ }
355
+
356
+ function MiniCard({ title, value, detail }: { title: string; value: string; detail: string }) {
357
+ return (
358
+ <div className="rounded-2xl border border-white/10 bg-background/40 p-4">
359
+ <div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">{title}</div>
360
+ <div className="mt-2 font-display text-2xl font-bold">{value}</div>
361
+ <div className="mt-1 text-xs text-muted-foreground">{detail}</div>
362
+ </div>
363
+ );
364
+ }
365
+
366
+ function ActionLink({
367
+ to,
368
+ icon,
369
+ title,
370
+ description,
371
+ hidden,
372
+ }: {
373
+ to: "/saved" | "/categories" | "/admin";
374
+ icon: React.ReactNode;
375
+ title: string;
376
+ description: string;
377
+ hidden?: boolean;
378
+ }) {
379
+ if (hidden) return null;
380
+
381
+ return (
382
+ <Link
383
+ to={to}
384
+ className="flex items-start gap-3 rounded-2xl border border-white/10 bg-background/40 p-4 transition hover:bg-white/5"
385
+ >
386
+ <div className="mt-0.5 rounded-full bg-brand-magenta/15 p-2 text-brand-magenta">{icon}</div>
387
+ <div className="min-w-0">
388
+ <div className="font-medium">{title}</div>
389
+ <div className="mt-1 text-sm text-muted-foreground">{description}</div>
390
+ </div>
391
+ </Link>
392
+ );
393
+ }
@@ -0,0 +1,71 @@
1
+ import { createFileRoute, Link } from "@tanstack/react-router";
2
+ import { useState } from "react";
3
+ import { supabase } from "@/integrations/supabase/client";
4
+ import { Logo } from "@/components/scrollsy/Logo";
5
+ import { Field } from "./login";
6
+ import { toast } from "sonner";
7
+ import { Mail, ArrowRight } from "lucide-react";
8
+
9
+ export const Route = createFileRoute("/reset-password")({
10
+ head: () => ({ meta: [{ title: "Reset password · Scrollsy" }] }),
11
+ component: ResetPassword,
12
+ });
13
+
14
+ function ResetPassword() {
15
+ const [email, setEmail] = useState("");
16
+ const [loading, setLoading] = useState(false);
17
+ const [submitted, setSubmitted] = useState(false);
18
+
19
+ const onSubmit = async (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ if (!email) return toast.error("Enter the email for your account.");
22
+ setLoading(true);
23
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
24
+ redirectTo: `${window.location.origin}/update-password`,
25
+ });
26
+ setLoading(false);
27
+ if (error) {
28
+ return toast.error(error.message || "Unable to send reset email.");
29
+ }
30
+ setSubmitted(true);
31
+ toast.success("Check your email for a password reset link.");
32
+ };
33
+
34
+ return (
35
+ <div className="min-h-[80vh] flex items-center justify-center px-4 py-16">
36
+ <div className="w-full max-w-md glass-strong rounded-3xl p-8 shadow-elevated animate-scale-in">
37
+ <div className="flex justify-center mb-2">
38
+ <Logo size={40} />
39
+ </div>
40
+ <h1 className="font-display text-3xl font-bold text-center mt-4">Forgot your password?</h1>
41
+ <p className="text-center text-sm text-muted-foreground mt-1">
42
+ Enter the email tied to your Scrollsy account and we’ll send a secure reset link.
43
+ </p>
44
+
45
+ <form onSubmit={onSubmit} className="space-y-3 mt-6">
46
+ <Field
47
+ icon={<Mail className="w-4 h-4" />}
48
+ type="email"
49
+ value={email}
50
+ onChange={setEmail}
51
+ placeholder="Email"
52
+ />
53
+ <button
54
+ disabled={loading || submitted}
55
+ className="btn-glow w-full rounded-full py-3 font-semibold text-sm inline-flex items-center justify-center gap-2 disabled:opacity-60"
56
+ >
57
+ <ArrowRight className="w-4 h-4" />{" "}
58
+ {loading ? "Sending…" : submitted ? "Email sent" : "Send reset link"}
59
+ </button>
60
+ </form>
61
+
62
+ <p className="mt-6 text-sm text-center text-muted-foreground">
63
+ Remembered it?{" "}
64
+ <Link to="/login" className="text-foreground font-semibold hover:text-gradient">
65
+ Sign in
66
+ </Link>
67
+ </p>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,83 @@
1
+ import { createFileRoute, Link } from "@tanstack/react-router";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { supabase } from "@/integrations/supabase/client";
4
+ import { useAuth } from "@/hooks/use-auth";
5
+ import { ProductCard } from "@/components/scrollsy/ProductCard";
6
+ import { Heart } from "lucide-react";
7
+
8
+ export const Route = createFileRoute("/saved")({
9
+ head: () => ({ meta: [{ title: "Saved · Scrollsy" }] }),
10
+ component: Saved,
11
+ });
12
+
13
+ function Saved() {
14
+ const { user, loading } = useAuth();
15
+
16
+ const { data: products, isLoading } = useQuery({
17
+ queryKey: ["saved-products", user?.id],
18
+ enabled: !!user,
19
+ queryFn: async () => {
20
+ const { data } = await supabase
21
+ .from("saved_items")
22
+ .select("created_at, products(*)")
23
+ .eq("user_id", user!.id)
24
+ .order("created_at", { ascending: false });
25
+ return (data ?? []).map((r: any) => r.products).filter(Boolean);
26
+ },
27
+ });
28
+
29
+ if (loading) return <div className="px-4 max-w-7xl mx-auto pt-10">Loading…</div>;
30
+
31
+ if (!user) {
32
+ return (
33
+ <div className="max-w-md mx-auto pt-24 px-4 text-center">
34
+ <Heart className="w-12 h-12 mx-auto text-brand-magenta" />
35
+ <h1 className="font-display text-3xl font-bold mt-4">Save your favorite finds</h1>
36
+ <p className="text-muted-foreground mt-2">Sign in to start building your wishlist.</p>
37
+ <Link
38
+ to="/login"
39
+ className="btn-glow inline-block mt-6 rounded-full px-6 py-3 font-semibold text-sm"
40
+ >
41
+ Sign in
42
+ </Link>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <div className="px-4 max-w-7xl mx-auto pt-10">
49
+ <h1 className="font-display text-4xl md:text-5xl font-bold tracking-tight">
50
+ Your <span className="text-gradient">saved</span>
51
+ </h1>
52
+ <p className="text-muted-foreground mt-2">{products?.length ?? 0} finds in your collection</p>
53
+
54
+ {isLoading ? (
55
+ <div className="mt-10 grid grid-cols-2 md:grid-cols-4 gap-4">
56
+ {Array.from({ length: 4 }).map((_, i) => (
57
+ <div key={i} className="aspect-[3/4] rounded-3xl shimmer bg-surface-elevated" />
58
+ ))}
59
+ </div>
60
+ ) : (products?.length ?? 0) === 0 ? (
61
+ <div className="glass rounded-3xl p-12 text-center mt-10">
62
+ <div className="text-5xl mb-3">💝</div>
63
+ <h3 className="font-display text-xl font-semibold">No saved items yet</h3>
64
+ <p className="text-sm text-muted-foreground mt-1 mb-5">
65
+ Tap the heart on any find to save it here.
66
+ </p>
67
+ <Link
68
+ to="/"
69
+ className="btn-glow inline-block rounded-full px-5 py-2.5 text-sm font-semibold"
70
+ >
71
+ Start discovering
72
+ </Link>
73
+ </div>
74
+ ) : (
75
+ <div className="mt-8 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
76
+ {products!.map((p: any) => (
77
+ <ProductCard key={p.id} product={p} aspect="tall" />
78
+ ))}
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,136 @@
1
+ import { createFileRoute, useNavigate } from "@tanstack/react-router";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { searchProducts } from "@/lib/products.functions";
4
+ import { ProductCard } from "@/components/scrollsy/ProductCard";
5
+ import { Search as SearchIcon } from "lucide-react";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { z } from "zod";
8
+ import {
9
+ applyProductFilters,
10
+ defaultProductFilters,
11
+ ProductFilters,
12
+ type FilterableProduct,
13
+ type ProductFiltersState,
14
+ } from "@/components/scrollsy/ProductFilters";
15
+
16
+ const searchSchema = z.object({ q: z.string().optional().default("") });
17
+
18
+ export const Route = createFileRoute("/search")({
19
+ validateSearch: (s) => searchSchema.parse(s),
20
+ head: () => ({
21
+ meta: [
22
+ { title: "Search · Scrollsy" },
23
+ { name: "description", content: "Search trending products and viral finds on Scrollsy." },
24
+ ],
25
+ }),
26
+ component: SearchPage,
27
+ });
28
+
29
+ function SearchPage() {
30
+ const { q } = Route.useSearch();
31
+ const nav = useNavigate();
32
+ const [input, setInput] = useState(q);
33
+ const [filters, setFilters] = useState<ProductFiltersState>(defaultProductFilters());
34
+
35
+ useEffect(() => setInput(q), [q]);
36
+
37
+ const { data, isLoading } = useQuery({
38
+ queryKey: ["search", q],
39
+ queryFn: () => searchProducts({ data: { q } }),
40
+ });
41
+
42
+ const products = useMemo(() => (data ?? []) as FilterableProduct[], [data]);
43
+ const filteredProducts = useMemo(
44
+ () => applyProductFilters(products, filters),
45
+ [products, filters],
46
+ );
47
+
48
+ return (
49
+ <div className="px-4 max-w-7xl mx-auto pt-10 pb-16">
50
+ <div className="flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
51
+ <div>
52
+ <h1 className="font-display text-4xl md:text-5xl font-bold tracking-tight">
53
+ {q ? (
54
+ <>
55
+ Results for <span className="text-gradient">"{q}"</span>
56
+ </>
57
+ ) : (
58
+ "Discover trending finds"
59
+ )}
60
+ </h1>
61
+ <p className="text-sm text-muted-foreground mt-2">
62
+ Shop the web like a marketplace — sort, filter, and compare at a glance.
63
+ </p>
64
+ </div>
65
+
66
+ <div className="text-sm text-muted-foreground">
67
+ {isLoading ? "Searching…" : `${filteredProducts.length} products found`}
68
+ </div>
69
+ </div>
70
+
71
+ <form
72
+ onSubmit={(e) => {
73
+ e.preventDefault();
74
+ nav({ to: "/search", search: { q: input } });
75
+ }}
76
+ className="mt-6 flex glass-strong rounded-2xl p-1.5 max-w-2xl"
77
+ >
78
+ <div className="px-3 flex items-center">
79
+ <SearchIcon className="w-5 h-5 text-muted-foreground" />
80
+ </div>
81
+ <input
82
+ autoFocus
83
+ value={input}
84
+ onChange={(e) => setInput(e.target.value)}
85
+ placeholder="Search anything…"
86
+ className="flex-1 bg-transparent outline-none text-sm py-3"
87
+ />
88
+ <button className="btn-glow rounded-xl px-5 text-sm font-semibold">Search</button>
89
+ </form>
90
+
91
+ <div className="mt-6">
92
+ <ProductFilters
93
+ filters={filters}
94
+ onChange={setFilters}
95
+ resultCount={filteredProducts.length}
96
+ />
97
+ </div>
98
+
99
+ <div className="mt-8">
100
+ {isLoading ? (
101
+ <SkeletonGrid />
102
+ ) : filteredProducts.length === 0 ? (
103
+ <EmptyState />
104
+ ) : (
105
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
106
+ {filteredProducts.map((p) => (
107
+ <ProductCard key={p.id} product={p as any} aspect="tall" />
108
+ ))}
109
+ </div>
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function SkeletonGrid() {
117
+ return (
118
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
119
+ {Array.from({ length: 8 }).map((_, i) => (
120
+ <div key={i} className="aspect-[3/4] rounded-3xl shimmer bg-surface-elevated" />
121
+ ))}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function EmptyState() {
127
+ return (
128
+ <div className="glass rounded-3xl p-12 text-center">
129
+ <div className="text-6xl mb-3">🔍</div>
130
+ <h3 className="font-display text-xl font-semibold">No finds match that search</h3>
131
+ <p className="text-sm text-muted-foreground mt-1">
132
+ Try a different keyword or relax a few filters.
133
+ </p>
134
+ </div>
135
+ );
136
+ }