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,564 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware";
3
+ import { supabaseAdmin } from "@/integrations/supabase/client.server";
4
+ import { z } from "zod";
5
+
6
+ const MASTER_ADMIN_ID = "doraexplora";
7
+ const FETCH_TIMEOUT_MS = 12_000;
8
+
9
+ type RoleRow = {
10
+ admin_id: string | null;
11
+ };
12
+
13
+ type UserRow = {
14
+ id: string;
15
+ email: string | null;
16
+ };
17
+
18
+ type ProductImportPreview = {
19
+ title: string;
20
+ description: string | null;
21
+ image_url: string;
22
+ price: number;
23
+ original_price: number | null;
24
+ currency: string;
25
+ merchant: string | null;
26
+ product_url: string;
27
+ source_url: string | null;
28
+ tracking_url: string | null;
29
+ imported_from: string | null;
30
+ tags: string[];
31
+ badges: string[];
32
+ shipping_info: string | null;
33
+ delivery_estimate: string | null;
34
+ import_notes: string | null;
35
+ };
36
+
37
+ async function getAdminRole(supabase: any, userId: string) {
38
+ const { data } = await supabase
39
+ .from("user_roles")
40
+ .select("admin_id")
41
+ .eq("user_id", userId)
42
+ .eq("role", "admin")
43
+ .maybeSingle();
44
+ return data as RoleRow | null;
45
+ }
46
+
47
+ async function requireAdmin(supabase: any, userId: string) {
48
+ const role = await getAdminRole(supabase, userId);
49
+ if (!role) throw new Error("Forbidden");
50
+ return role.admin_id;
51
+ }
52
+
53
+ async function findAuthUserByEmail(email: string) {
54
+ const normalizedEmail = email.trim().toLowerCase();
55
+ const { data, error } = await supabaseAdmin.auth.admin.listUsers({ perPage: 1000 });
56
+ if (error) throw new Error(error.message);
57
+ const user = data.users.find((candidate) => candidate.email?.toLowerCase() === normalizedEmail);
58
+ return (user ?? null) as UserRow | null;
59
+ }
60
+
61
+ function normalizeUrl(value: string) {
62
+ const trimmed = value.trim();
63
+ if (!trimmed) return trimmed;
64
+ if (trimmed.startsWith("//")) return `https:${trimmed}`;
65
+ return trimmed;
66
+ }
67
+
68
+ function getHostnameLabel(input: string) {
69
+ try {
70
+ return new URL(input).hostname.replace(/^www\./, "");
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function safeText(value: string | undefined | null) {
77
+ const text = value?.replace(/\s+/g, " ").trim();
78
+ return text ? text : null;
79
+ }
80
+
81
+ function firstMatch(html: string, patterns: RegExp[]) {
82
+ for (const pattern of patterns) {
83
+ const match = html.match(pattern);
84
+ if (match?.[1]) return safeText(match[1]);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function extractMetaContent(html: string, key: string) {
90
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ return firstMatch(html, [
92
+ new RegExp(`<meta[^>]+property=["']${escaped}["'][^>]+content=["']([^"']+)["'][^>]*>`, "i"),
93
+ new RegExp(`<meta[^>]+name=["']${escaped}["'][^>]+content=["']([^"']+)["'][^>]*>`, "i"),
94
+ new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+property=["']${escaped}["'][^>]*>`, "i"),
95
+ new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+name=["']${escaped}["'][^>]*>`, "i"),
96
+ ]);
97
+ }
98
+
99
+ function extractJsonLdObjects(html: string) {
100
+ const matches = [
101
+ ...html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi),
102
+ ];
103
+ const parsed: unknown[] = [];
104
+
105
+ for (const match of matches) {
106
+ const raw = safeText(match[1]);
107
+ if (!raw) continue;
108
+ try {
109
+ const json = JSON.parse(raw);
110
+ if (Array.isArray(json)) parsed.push(...json);
111
+ else parsed.push(json);
112
+ } catch {
113
+ continue;
114
+ }
115
+ }
116
+
117
+ return parsed;
118
+ }
119
+
120
+ function pickJsonLdProduct(html: string) {
121
+ const objects = extractJsonLdObjects(html);
122
+ for (const object of objects) {
123
+ if (!object || typeof object !== "object") continue;
124
+ const candidate = object as Record<string, unknown>;
125
+ const type = candidate["@type"];
126
+ if (type === "Product") return candidate;
127
+ if (Array.isArray(type) && type.includes("Product")) return candidate;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function toNumber(value: unknown) {
133
+ if (typeof value === "number" && Number.isFinite(value)) return value;
134
+ if (typeof value === "string") {
135
+ const parsed = Number(value.replace(/[^\d.-]/g, ""));
136
+ return Number.isFinite(parsed) ? parsed : null;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function extractProductPreview(url: string, html: string): ProductImportPreview {
142
+ const hostname = getHostnameLabel(url);
143
+ const jsonLd = pickJsonLdProduct(html);
144
+
145
+ const title =
146
+ safeText((jsonLd?.name as string | undefined) ?? undefined) ||
147
+ extractMetaContent(html, "og:title") ||
148
+ extractMetaContent(html, "twitter:title") ||
149
+ firstMatch(html, [/<title[^>]*>([\s\S]*?)<\/title>/i]) ||
150
+ hostname ||
151
+ "Imported product";
152
+
153
+ const description =
154
+ safeText((jsonLd?.description as string | undefined) ?? undefined) ||
155
+ extractMetaContent(html, "og:description") ||
156
+ extractMetaContent(html, "twitter:description") ||
157
+ extractMetaContent(html, "description");
158
+
159
+ const imageFromJsonLd = jsonLd?.image;
160
+ const image_url =
161
+ (Array.isArray(imageFromJsonLd)
162
+ ? safeText(imageFromJsonLd[0] as string | undefined)
163
+ : safeText(imageFromJsonLd as string | undefined)) ||
164
+ extractMetaContent(html, "og:image") ||
165
+ extractMetaContent(html, "twitter:image") ||
166
+ extractMetaContent(html, "twitter:image:src") ||
167
+ "https://images.unsplash.com/photo-1558862107-d49ef2a04d72?auto=format&fit=crop&w=1200&q=80";
168
+
169
+ const offers = jsonLd?.offers;
170
+ const offer = Array.isArray(offers) ? offers[0] : offers;
171
+ const price =
172
+ toNumber((offer as Record<string, unknown> | undefined)?.price) ??
173
+ toNumber(extractMetaContent(html, "product:price:amount")) ??
174
+ toNumber(extractMetaContent(html, "price")) ??
175
+ 0;
176
+
177
+ const originalPrice =
178
+ toNumber(extractMetaContent(html, "product:original_price")) ??
179
+ toNumber(extractMetaContent(html, "og:price:amount")) ??
180
+ null;
181
+
182
+ const currency =
183
+ safeText((offer as Record<string, unknown> | undefined)?.priceCurrency as string | undefined) ||
184
+ extractMetaContent(html, "product:price:currency") ||
185
+ "USD";
186
+
187
+ const merchant =
188
+ safeText((jsonLd?.brand as Record<string, unknown> | undefined)?.name as string | undefined) ||
189
+ safeText((jsonLd?.seller as Record<string, unknown> | undefined)?.name as string | undefined) ||
190
+ hostname;
191
+
192
+ const aggregateRating = jsonLd?.aggregateRating as Record<string, unknown> | undefined;
193
+ const ratingValue = toNumber(aggregateRating?.ratingValue);
194
+ const ratingCount =
195
+ toNumber(aggregateRating?.ratingCount) ?? toNumber(aggregateRating?.reviewCount);
196
+
197
+ const badges = [
198
+ ratingValue ? `${ratingValue.toFixed(1)}★` : null,
199
+ ratingCount ? `${Math.round(ratingCount)} reviews` : null,
200
+ ].filter(Boolean) as string[];
201
+
202
+ const imported_from = hostname;
203
+ const notes = `Imported from ${hostname ?? "shared link"}`;
204
+
205
+ return {
206
+ title,
207
+ description,
208
+ image_url,
209
+ price,
210
+ original_price: originalPrice,
211
+ currency,
212
+ merchant,
213
+ product_url: url,
214
+ source_url: url,
215
+ tracking_url: url,
216
+ imported_from,
217
+ tags: hostname ? [hostname.split(".")[0]] : [],
218
+ badges,
219
+ shipping_info: null,
220
+ delivery_estimate: null,
221
+ import_notes: notes,
222
+ };
223
+ }
224
+
225
+ async function fetchImportHtml(url: string) {
226
+ const controller = new AbortController();
227
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
228
+
229
+ try {
230
+ const response = await fetch(url, {
231
+ headers: {
232
+ "user-agent":
233
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36",
234
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
235
+ },
236
+ signal: controller.signal,
237
+ });
238
+
239
+ if (!response.ok) {
240
+ throw new Error(`Import failed with status ${response.status}`);
241
+ }
242
+
243
+ const contentType = response.headers.get("content-type") ?? "";
244
+ if (!contentType.includes("text/html") && !contentType.includes("application/xhtml+xml")) {
245
+ throw new Error("The provided link does not look like a product page");
246
+ }
247
+
248
+ return await response.text();
249
+ } finally {
250
+ clearTimeout(timeout);
251
+ }
252
+ }
253
+
254
+ export const isAdmin = createServerFn({ method: "GET" })
255
+ .middleware([requireSupabaseAuth])
256
+ .handler(async ({ context }) => {
257
+ const { supabase, userId } = context;
258
+ const role = await getAdminRole(supabase, userId);
259
+ return {
260
+ admin: !!role,
261
+ admin_id: role?.admin_id ?? null,
262
+ is_master: role?.admin_id === MASTER_ADMIN_ID,
263
+ };
264
+ });
265
+
266
+ export const getAdminBootstrapStatus = createServerFn({ method: "GET" }).handler(async () => {
267
+ const { count, error } = await supabaseAdmin
268
+ .from("user_roles")
269
+ .select("*", { count: "exact", head: true })
270
+ .eq("role", "admin");
271
+
272
+ if (error) throw new Error(error.message);
273
+
274
+ return {
275
+ needs_bootstrap: (count ?? 0) === 0,
276
+ };
277
+ });
278
+
279
+ export const bootstrapMasterAdmin = createServerFn({ method: "POST" })
280
+ .inputValidator((d) =>
281
+ z
282
+ .object({
283
+ email: z.string().email().max(255),
284
+ password: z.string().min(8).max(128),
285
+ })
286
+ .parse(d),
287
+ )
288
+ .handler(async ({ data }) => {
289
+ const { count, error: countError } = await supabaseAdmin
290
+ .from("user_roles")
291
+ .select("*", { count: "exact", head: true })
292
+ .eq("role", "admin");
293
+
294
+ if (countError) throw new Error(countError.message);
295
+ if ((count ?? 0) > 0) {
296
+ throw new Error("Master Admin is already set up.");
297
+ }
298
+
299
+ const existingUser = await findAuthUserByEmail(data.email);
300
+
301
+ let userId = existingUser?.id;
302
+ let createdUser = false;
303
+
304
+ if (!userId) {
305
+ const { data: created, error: createError } = await supabaseAdmin.auth.admin.createUser({
306
+ email: data.email,
307
+ password: data.password,
308
+ email_confirm: true,
309
+ });
310
+
311
+ if (createError || !created.user) {
312
+ throw new Error(createError?.message ?? "Failed to create master admin account");
313
+ }
314
+
315
+ userId = created.user.id;
316
+ createdUser = true;
317
+ }
318
+
319
+ const { error: roleError } = await supabaseAdmin.from("user_roles").insert({
320
+ user_id: userId,
321
+ role: "admin",
322
+ admin_id: MASTER_ADMIN_ID,
323
+ });
324
+
325
+ if (roleError) {
326
+ if (createdUser) {
327
+ await supabaseAdmin.auth.admin.deleteUser(userId);
328
+ }
329
+ throw new Error(roleError.message);
330
+ }
331
+
332
+ return {
333
+ ok: true,
334
+ user_id: userId,
335
+ admin_id: MASTER_ADMIN_ID,
336
+ created_user: createdUser,
337
+ };
338
+ });
339
+
340
+ export const importProductFromLink = createServerFn({ method: "POST" })
341
+ .middleware([requireSupabaseAuth])
342
+ .inputValidator((d) =>
343
+ z
344
+ .object({
345
+ url: z.string().url().max(2000),
346
+ })
347
+ .parse(d),
348
+ )
349
+ .handler(async ({ data, context }) => {
350
+ await requireAdmin(context.supabase, context.userId);
351
+ const url = normalizeUrl(data.url);
352
+ const html = await fetchImportHtml(url);
353
+ return extractProductPreview(url, html);
354
+ });
355
+
356
+ export const upsertProduct = createServerFn({ method: "POST" })
357
+ .middleware([requireSupabaseAuth])
358
+ .inputValidator((d) => productSchema.parse(d))
359
+ .handler(async ({ data, context }) => {
360
+ await requireAdmin(context.supabase, context.userId);
361
+
362
+ const product_url = normalizeUrl(
363
+ data.product_url ?? data.source_url ?? data.tracking_url ?? data.affiliate_url ?? "",
364
+ );
365
+ const source_url = normalizeUrl(
366
+ data.source_url ?? data.product_url ?? data.affiliate_url ?? product_url,
367
+ );
368
+ const tracking_url = normalizeUrl(data.tracking_url ?? data.affiliate_url ?? product_url);
369
+ const affiliate_url = normalizeUrl(data.affiliate_url ?? tracking_url ?? product_url);
370
+
371
+ const payload = {
372
+ ...data,
373
+ product_url,
374
+ source_url,
375
+ tracking_url,
376
+ affiliate_url,
377
+ imported_from: data.imported_from ?? getHostnameLabel(source_url ?? product_url),
378
+ import_notes: data.import_notes ?? null,
379
+ };
380
+
381
+ if (payload.id) {
382
+ const { error } = await context.supabase
383
+ .from("products")
384
+ .update(payload)
385
+ .eq("id", payload.id);
386
+ if (error) throw new Error(error.message);
387
+ return { id: payload.id };
388
+ }
389
+
390
+ const { data: ins, error } = await context.supabase
391
+ .from("products")
392
+ .insert(payload)
393
+ .select("id")
394
+ .single();
395
+ if (error) throw new Error(error.message);
396
+ return { id: ins.id };
397
+ });
398
+
399
+ export const deleteProduct = createServerFn({ method: "POST" })
400
+ .middleware([requireSupabaseAuth])
401
+ .inputValidator((d: { id: string }) => z.object({ id: z.string().uuid() }).parse(d))
402
+ .handler(async ({ data, context }) => {
403
+ await requireAdmin(context.supabase, context.userId);
404
+ const { error } = await context.supabase.from("products").delete().eq("id", data.id);
405
+ if (error) throw new Error(error.message);
406
+ return { ok: true };
407
+ });
408
+
409
+ export const listAllProducts = createServerFn({ method: "GET" })
410
+ .middleware([requireSupabaseAuth])
411
+ .handler(async ({ context }) => {
412
+ await requireAdmin(context.supabase, context.userId);
413
+ const { data } = await context.supabase
414
+ .from("products")
415
+ .select("*")
416
+ .order("created_at", { ascending: false });
417
+ return data ?? [];
418
+ });
419
+
420
+ const productSchema = z
421
+ .object({
422
+ id: z.string().uuid().optional(),
423
+ title: z.string().min(1).max(200),
424
+ description: z.string().max(2000).optional().nullable(),
425
+ image_url: z.string().url().max(2000),
426
+ price: z.number().min(0).max(100000),
427
+ original_price: z.number().min(0).max(100000).optional().nullable(),
428
+ currency: z.string().min(1).max(12).default("USD"),
429
+ affiliate_url: z.string().url().max(2000).optional().nullable(),
430
+ product_url: z.string().url().max(2000).optional().nullable(),
431
+ tracking_url: z.string().url().max(2000).optional().nullable(),
432
+ source_url: z.string().url().max(2000).optional().nullable(),
433
+ imported_from: z.string().max(120).optional().nullable(),
434
+ import_notes: z.string().max(500).optional().nullable(),
435
+ merchant: z.string().max(100).optional().nullable(),
436
+ category_id: z.string().uuid().optional().nullable(),
437
+ tags: z.array(z.string().max(40)).max(20).default([]),
438
+ badges: z.array(z.string().max(40)).max(10).default([]),
439
+ is_trending: z.boolean().default(false),
440
+ is_hot: z.boolean().default(false),
441
+ is_featured: z.boolean().default(false),
442
+ shipping_info: z.string().max(120).optional().nullable(),
443
+ delivery_estimate: z.string().max(60).optional().nullable(),
444
+ })
445
+ .superRefine((value, ctx) => {
446
+ if (!value.product_url && !value.source_url && !value.tracking_url && !value.affiliate_url) {
447
+ ctx.addIssue({
448
+ code: z.ZodIssueCode.custom,
449
+ path: ["product_url"],
450
+ message: "Provide a product URL, source URL, tracking URL, or affiliate URL",
451
+ });
452
+ }
453
+ });
454
+
455
+ /**
456
+ * Master admin bootstrap for legacy installs.
457
+ * Used only while no admin exists yet.
458
+ */
459
+ export const grantSelfAdmin = createServerFn({ method: "POST" })
460
+ .middleware([requireSupabaseAuth])
461
+ .handler(async ({ context }) => {
462
+ const { supabase, userId } = context;
463
+ const { count, error } = await supabase
464
+ .from("user_roles")
465
+ .select("*", { count: "exact", head: true })
466
+ .eq("role", "admin");
467
+ if (error) throw new Error(error.message);
468
+ if ((count ?? 0) > 0)
469
+ throw new Error("Admin already exists. Ask the Master Admin to grant you access.");
470
+ const { error: insertError } = await supabaseAdmin
471
+ .from("user_roles")
472
+ .insert({ user_id: userId, role: "admin", admin_id: MASTER_ADMIN_ID });
473
+ if (insertError) throw new Error(insertError.message);
474
+ return { ok: true, admin_id: MASTER_ADMIN_ID };
475
+ });
476
+
477
+ /**
478
+ * Master admin creates a new admin account: provisions the auth user with email+password,
479
+ * assigns the chosen admin_id. Only callable by the Master Admin (doraexplora).
480
+ */
481
+ export const createAdmin = createServerFn({ method: "POST" })
482
+ .middleware([requireSupabaseAuth])
483
+ .inputValidator((d) =>
484
+ z
485
+ .object({
486
+ email: z.string().email().max(255),
487
+ password: z.string().min(8).max(128),
488
+ admin_id: z
489
+ .string()
490
+ .min(3)
491
+ .max(40)
492
+ .regex(/^[a-zA-Z0-9_-]+$/, "Admin ID may only contain letters, numbers, _ and -"),
493
+ })
494
+ .parse(d),
495
+ )
496
+ .handler(async ({ data, context }) => {
497
+ const callerAdminId = await requireAdmin(context.supabase, context.userId);
498
+ if (callerAdminId !== MASTER_ADMIN_ID)
499
+ throw new Error("Only the Master Admin can create new admins");
500
+ if (data.admin_id === MASTER_ADMIN_ID) throw new Error("That admin ID is reserved");
501
+
502
+ const { data: existing } = await supabaseAdmin
503
+ .from("user_roles")
504
+ .select("id")
505
+ .eq("admin_id", data.admin_id)
506
+ .maybeSingle();
507
+ if (existing) throw new Error("Admin ID is already taken");
508
+
509
+ const { data: created, error: createErr } = await supabaseAdmin.auth.admin.createUser({
510
+ email: data.email,
511
+ password: data.password,
512
+ email_confirm: true,
513
+ });
514
+ if (createErr || !created.user) throw new Error(createErr?.message ?? "Failed to create user");
515
+
516
+ const newUserId = created.user.id;
517
+
518
+ const { error: roleErr } = await supabaseAdmin
519
+ .from("user_roles")
520
+ .insert({ user_id: newUserId, role: "admin", admin_id: data.admin_id });
521
+ if (roleErr) {
522
+ await supabaseAdmin.auth.admin.deleteUser(newUserId);
523
+ throw new Error(roleErr.message);
524
+ }
525
+
526
+ return { ok: true, user_id: newUserId, admin_id: data.admin_id };
527
+ });
528
+
529
+ export const listAdmins = createServerFn({ method: "GET" })
530
+ .middleware([requireSupabaseAuth])
531
+ .handler(async ({ context }) => {
532
+ await requireAdmin(context.supabase, context.userId);
533
+ const { data: roles } = await supabaseAdmin
534
+ .from("user_roles")
535
+ .select("user_id, admin_id, created_at")
536
+ .eq("role", "admin")
537
+ .order("created_at", { ascending: true });
538
+ if (!roles?.length) return [];
539
+ const { data: users } = await supabaseAdmin.auth.admin.listUsers({ perPage: 1000 });
540
+ const emailById = new Map(users.users.map((u) => [u.id, u.email]));
541
+ return roles.map((r) => ({
542
+ user_id: r.user_id,
543
+ admin_id: r.admin_id,
544
+ email: emailById.get(r.user_id) ?? null,
545
+ is_master: r.admin_id === MASTER_ADMIN_ID,
546
+ created_at: r.created_at,
547
+ }));
548
+ });
549
+
550
+ /**
551
+ * Admin login verification: signed-in user submits the admin_id they were assigned.
552
+ * Returns ok if the user is an admin AND the admin_id matches their record.
553
+ */
554
+ export const verifyAdminId = createServerFn({ method: "POST" })
555
+ .middleware([requireSupabaseAuth])
556
+ .inputValidator((d) => z.object({ admin_id: z.string().min(1).max(40) }).parse(d))
557
+ .handler(async ({ data, context }) => {
558
+ const { supabase, userId } = context;
559
+ const row = await getAdminRole(supabase, userId);
560
+ if (!row || row.admin_id !== data.admin_id) {
561
+ throw new Error("Invalid admin credentials");
562
+ }
563
+ return { ok: true, is_master: row.admin_id === MASTER_ADMIN_ID };
564
+ });
@@ -0,0 +1,27 @@
1
+ // Captures the original Error out-of-band so server.ts can recover the stack
2
+ // when h3 has already swallowed the throw into a generic 500 Response.
3
+
4
+ let lastCapturedError: { error: unknown; at: number } | undefined;
5
+ const TTL_MS = 5_000;
6
+
7
+ function record(error: unknown) {
8
+ lastCapturedError = { error, at: Date.now() };
9
+ }
10
+
11
+ if (typeof globalThis.addEventListener === "function") {
12
+ globalThis.addEventListener("error", (event) => record((event as ErrorEvent).error ?? event));
13
+ globalThis.addEventListener("unhandledrejection", (event) =>
14
+ record((event as PromiseRejectionEvent).reason),
15
+ );
16
+ }
17
+
18
+ export function consumeLastCapturedError(): unknown {
19
+ if (!lastCapturedError) return undefined;
20
+ if (Date.now() - lastCapturedError.at > TTL_MS) {
21
+ lastCapturedError = undefined;
22
+ return undefined;
23
+ }
24
+ const { error } = lastCapturedError;
25
+ lastCapturedError = undefined;
26
+ return error;
27
+ }
@@ -0,0 +1,30 @@
1
+ export function renderErrorPage(): string {
2
+ return `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>This page didn't load</title>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <style>
9
+ body { font: 15px/1.5 system-ui, -apple-system, sans-serif; background: #fafafa; color: #111; display: grid; place-items: center; min-height: 100vh; margin: 0; padding: 1.5rem; }
10
+ .card { max-width: 28rem; width: 100%; text-align: center; padding: 2rem; }
11
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
12
+ p { color: #4b5563; margin: 0 0 1.5rem; }
13
+ .actions { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; }
14
+ a, button { padding: 0.5rem 1rem; border-radius: 0.375rem; font: inherit; cursor: pointer; text-decoration: none; border: 1px solid transparent; }
15
+ .primary { background: #111; color: #fff; }
16
+ .secondary { background: #fff; color: #111; border-color: #d1d5db; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="card">
21
+ <h1>This page didn't load</h1>
22
+ <p>Something went wrong on our end. You can try refreshing or head back home.</p>
23
+ <div class="actions">
24
+ <button class="primary" onclick="location.reload()">Try again</button>
25
+ <a class="secondary" href="/">Go home</a>
26
+ </div>
27
+ </div>
28
+ </body>
29
+ </html>`;
30
+ }
@@ -0,0 +1,39 @@
1
+ type ProductLinkLike = {
2
+ product_url?: string | null;
3
+ tracking_url?: string | null;
4
+ affiliate_url?: string | null;
5
+ source_url?: string | null;
6
+ imported_from?: string | null;
7
+ merchant?: string | null;
8
+ };
9
+
10
+ function normalizeUrl(value?: string | null) {
11
+ const trimmed = value?.trim();
12
+ if (!trimmed) return null;
13
+ if (trimmed.startsWith("//")) return `https:${trimmed}`;
14
+ return trimmed;
15
+ }
16
+
17
+ export function resolveBuyUrl(product: ProductLinkLike) {
18
+ return (
19
+ normalizeUrl(product.tracking_url) ??
20
+ normalizeUrl(product.affiliate_url) ??
21
+ normalizeUrl(product.product_url) ??
22
+ normalizeUrl(product.source_url) ??
23
+ null
24
+ );
25
+ }
26
+
27
+ export function resolveSourceUrl(product: ProductLinkLike) {
28
+ return (
29
+ normalizeUrl(product.product_url) ??
30
+ normalizeUrl(product.source_url) ??
31
+ normalizeUrl(product.tracking_url) ??
32
+ normalizeUrl(product.affiliate_url) ??
33
+ null
34
+ );
35
+ }
36
+
37
+ export function resolveProductLabel(product: ProductLinkLike) {
38
+ return product.merchant ?? product.imported_from ?? "shopping store";
39
+ }