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,811 @@
1
+ import { createFileRoute, Link } from "@tanstack/react-router";
2
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { useServerFn } from "@tanstack/react-start";
4
+ import {
5
+ isAdmin,
6
+ listAllProducts,
7
+ upsertProduct,
8
+ deleteProduct,
9
+ grantSelfAdmin,
10
+ createAdmin,
11
+ listAdmins,
12
+ importProductFromLink,
13
+ } from "@/lib/admin.functions";
14
+ import { getCategories } from "@/lib/products.functions";
15
+ import { useAuth } from "@/hooks/use-auth";
16
+ import { useMemo, useState } from "react";
17
+ import {
18
+ Plus,
19
+ Trash2,
20
+ Edit3,
21
+ Shield,
22
+ ExternalLink,
23
+ Flame,
24
+ TrendingUp,
25
+ Star,
26
+ UserPlus,
27
+ Crown,
28
+ KeyRound,
29
+ Mail,
30
+ Lock,
31
+ Loader2,
32
+ WandSparkles,
33
+ RefreshCw,
34
+ Link2,
35
+ FileDown,
36
+ Sparkles,
37
+ } from "lucide-react";
38
+ import { toast } from "sonner";
39
+ import { resolveBuyUrl } from "@/lib/product-links";
40
+
41
+ export const Route = createFileRoute("/admin")({
42
+ head: () => ({ meta: [{ title: "Admin · Scrollsy" }, { name: "robots", content: "noindex" }] }),
43
+ component: AdminPage,
44
+ });
45
+
46
+ function AdminPage() {
47
+ const { user, loading } = useAuth();
48
+ const isAdminFn = useServerFn(isAdmin);
49
+ const grantFn = useServerFn(grantSelfAdmin);
50
+
51
+ const {
52
+ data: roleData,
53
+ isLoading: roleLoading,
54
+ refetch,
55
+ } = useQuery({
56
+ queryKey: ["is-admin", user?.id],
57
+ enabled: !!user,
58
+ queryFn: () => isAdminFn(),
59
+ });
60
+
61
+ if (loading || (user && roleLoading)) return <div className="p-10">Loading…</div>;
62
+
63
+ if (!user) {
64
+ return (
65
+ <div className="max-w-md mx-auto pt-24 px-4 text-center glass-strong rounded-3xl p-8">
66
+ <Shield className="w-12 h-12 mx-auto text-brand-magenta" />
67
+ <h1 className="font-display text-2xl font-bold mt-4">Admin sign-in required</h1>
68
+ <p className="text-sm text-muted-foreground mt-2">
69
+ Admins must sign in with their assigned Admin ID, email and password.
70
+ </p>
71
+ <Link
72
+ to="/admin/login"
73
+ className="btn-glow inline-block mt-6 rounded-full px-6 py-3 font-semibold text-sm"
74
+ >
75
+ Go to admin sign-in
76
+ </Link>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (!roleData?.admin) {
82
+ return (
83
+ <div className="max-w-md mx-auto pt-24 px-4 text-center glass-strong rounded-3xl p-8">
84
+ <Shield className="w-10 h-10 mx-auto text-brand-magenta" />
85
+ <h1 className="font-display text-2xl font-bold mt-4">Not an admin yet</h1>
86
+ <p className="text-sm text-muted-foreground mt-2">
87
+ If you're the project owner, claim Master Admin access now. Only the very first user can
88
+ self-grant — you will receive the master Admin ID{" "}
89
+ <span className="text-foreground font-mono">doraexplora</span>.
90
+ </p>
91
+ <button
92
+ onClick={async () => {
93
+ try {
94
+ const res = await grantFn();
95
+ toast.success(`You're the Master Admin! Admin ID: ${res.admin_id}`);
96
+ refetch();
97
+ } catch (e: any) {
98
+ toast.error(e.message ?? "Failed");
99
+ }
100
+ }}
101
+ className="btn-glow mt-6 rounded-full px-6 py-3 font-semibold text-sm"
102
+ >
103
+ Claim Master Admin access
104
+ </button>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ return <AdminDashboard isMaster={!!roleData.is_master} adminId={roleData.admin_id} />;
110
+ }
111
+
112
+ function AdminDashboard({ isMaster, adminId }: { isMaster: boolean; adminId: string | null }) {
113
+ const qc = useQueryClient();
114
+ const listFn = useServerFn(listAllProducts);
115
+ const upsertFn = useServerFn(upsertProduct);
116
+ const delFn = useServerFn(deleteProduct);
117
+ const importFn = useServerFn(importProductFromLink);
118
+
119
+ const { data: products } = useQuery({ queryKey: ["admin-products"], queryFn: () => listFn() });
120
+ const { data: categories } = useQuery({ queryKey: ["cats"], queryFn: () => getCategories() });
121
+
122
+ const [editing, setEditing] = useState<any | null>(null);
123
+ const [tab, setTab] = useState<"products" | "admins">("products");
124
+
125
+ const upsert = useMutation({
126
+ mutationFn: (data: any) => upsertFn({ data }),
127
+ onSuccess: () => {
128
+ qc.invalidateQueries({ queryKey: ["admin-products"] });
129
+ qc.invalidateQueries({ queryKey: ["home"] });
130
+ toast.success("Saved");
131
+ setEditing(null);
132
+ },
133
+ onError: (e: any) => toast.error(e.message ?? "Failed"),
134
+ });
135
+
136
+ const del = useMutation({
137
+ mutationFn: (id: string) => delFn({ data: { id } }),
138
+ onSuccess: () => {
139
+ qc.invalidateQueries({ queryKey: ["admin-products"] });
140
+ toast.success("Deleted");
141
+ },
142
+ });
143
+
144
+ const stats = {
145
+ total: products?.length ?? 0,
146
+ trending: products?.filter((p) => p.is_trending).length ?? 0,
147
+ hot: products?.filter((p) => p.is_hot).length ?? 0,
148
+ featured: products?.filter((p) => p.is_featured).length ?? 0,
149
+ views: products?.reduce((s, p) => s + (p.view_count ?? 0), 0) ?? 0,
150
+ };
151
+
152
+ return (
153
+ <div className="max-w-7xl mx-auto px-4 pt-10 pb-20">
154
+ <div className="flex items-center justify-between flex-wrap gap-3">
155
+ <div>
156
+ <h1 className="font-display text-4xl font-bold">
157
+ Admin <span className="text-gradient">dashboard</span>
158
+ </h1>
159
+ <p className="text-sm text-muted-foreground mt-1 inline-flex items-center gap-2">
160
+ {isMaster && (
161
+ <span className="inline-flex items-center gap-1 text-brand-amber">
162
+ <Crown className="w-3.5 h-3.5" /> Master Admin
163
+ </span>
164
+ )}
165
+ <span>
166
+ Admin ID: <span className="font-mono text-foreground">{adminId}</span>
167
+ </span>
168
+ </p>
169
+ </div>
170
+ {tab === "products" && (
171
+ <button
172
+ onClick={() => setEditing({})}
173
+ className="btn-glow rounded-full px-5 py-2.5 font-semibold text-sm inline-flex items-center gap-2"
174
+ >
175
+ <Plus className="w-4 h-4" /> New product
176
+ </button>
177
+ )}
178
+ </div>
179
+
180
+ <div className="mt-6 inline-flex glass rounded-full p-1 text-sm">
181
+ <button
182
+ onClick={() => setTab("products")}
183
+ className={`px-4 py-1.5 rounded-full transition ${tab === "products" ? "bg-white/10 text-foreground" : "text-muted-foreground"}`}
184
+ >
185
+ Products
186
+ </button>
187
+ <button
188
+ onClick={() => setTab("admins")}
189
+ className={`px-4 py-1.5 rounded-full transition inline-flex items-center gap-1.5 ${tab === "admins" ? "bg-white/10 text-foreground" : "text-muted-foreground"}`}
190
+ >
191
+ <UserPlus className="w-3.5 h-3.5" /> Admins
192
+ </button>
193
+ </div>
194
+
195
+ {tab === "products" && (
196
+ <>
197
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-3 mt-6">
198
+ <Stat label="Products" value={stats.total} />
199
+ <Stat
200
+ label="Trending"
201
+ value={stats.trending}
202
+ icon={<TrendingUp className="w-4 h-4 text-brand-magenta" />}
203
+ />
204
+ <Stat
205
+ label="Hot deals"
206
+ value={stats.hot}
207
+ icon={<Flame className="w-4 h-4 text-brand-orange" />}
208
+ />
209
+ <Stat
210
+ label="Featured"
211
+ value={stats.featured}
212
+ icon={<Star className="w-4 h-4 text-brand-amber" />}
213
+ />
214
+ <Stat label="Total views" value={stats.views.toLocaleString()} />
215
+ </div>
216
+
217
+ <div className="mt-10 glass-strong rounded-3xl overflow-hidden">
218
+ <div className="grid grid-cols-12 px-5 py-3 text-xs text-muted-foreground uppercase tracking-wider border-b border-white/5">
219
+ <div className="col-span-5">Product</div>
220
+ <div className="col-span-2">Price</div>
221
+ <div className="col-span-2">Status</div>
222
+ <div className="col-span-1">Views</div>
223
+ <div className="col-span-2 text-right">Actions</div>
224
+ </div>
225
+ {(products ?? []).map((p) => {
226
+ const openUrl = resolveBuyUrl(p) ?? p.affiliate_url ?? p.product_url ?? p.source_url;
227
+ return (
228
+ <div
229
+ key={p.id}
230
+ className="grid grid-cols-12 px-5 py-3 items-center hover:bg-white/5 border-b border-white/5 last:border-none text-sm"
231
+ >
232
+ <div className="col-span-5 flex items-center gap-3 min-w-0">
233
+ <img
234
+ src={p.image_url}
235
+ alt=""
236
+ className="w-10 h-10 rounded-lg object-cover shrink-0"
237
+ />
238
+ <div className="truncate">
239
+ <div className="font-medium truncate">{p.title}</div>
240
+ <div className="text-xs text-muted-foreground truncate">
241
+ {p.imported_from ?? p.merchant ?? "Shopping site"}
242
+ </div>
243
+ </div>
244
+ </div>
245
+ <div className="col-span-2">
246
+ <span className="text-gradient font-bold">${Number(p.price).toFixed(2)}</span>
247
+ {p.original_price && (
248
+ <span className="text-xs text-muted-foreground line-through ml-2">
249
+ ${Number(p.original_price).toFixed(2)}
250
+ </span>
251
+ )}
252
+ </div>
253
+ <div className="col-span-2 flex gap-1 flex-wrap">
254
+ {p.is_trending && (
255
+ <span className="text-[10px] bg-brand-magenta/20 text-brand-magenta rounded px-1.5 py-0.5">
256
+ TREND
257
+ </span>
258
+ )}
259
+ {p.is_hot && (
260
+ <span className="text-[10px] bg-brand-orange/20 text-brand-orange rounded px-1.5 py-0.5">
261
+ HOT
262
+ </span>
263
+ )}
264
+ {p.is_featured && (
265
+ <span className="text-[10px] bg-brand-amber/20 text-brand-amber rounded px-1.5 py-0.5">
266
+ FEAT
267
+ </span>
268
+ )}
269
+ </div>
270
+ <div className="col-span-1 text-xs text-muted-foreground">{p.view_count}</div>
271
+ <div className="col-span-2 flex justify-end gap-1">
272
+ <a
273
+ href={openUrl ?? "#"}
274
+ target="_blank"
275
+ rel="noopener noreferrer"
276
+ className="p-2 rounded-lg hover:bg-white/10"
277
+ title="Open store link"
278
+ >
279
+ <ExternalLink className="w-4 h-4" />
280
+ </a>
281
+ <button
282
+ onClick={() => setEditing(p)}
283
+ className="p-2 rounded-lg hover:bg-white/10"
284
+ title="Edit"
285
+ >
286
+ <Edit3 className="w-4 h-4" />
287
+ </button>
288
+ <button
289
+ onClick={() => confirm("Delete this product?") && del.mutate(p.id)}
290
+ className="p-2 rounded-lg hover:bg-destructive/20 text-destructive"
291
+ title="Delete"
292
+ >
293
+ <Trash2 className="w-4 h-4" />
294
+ </button>
295
+ </div>
296
+ </div>
297
+ );
298
+ })}
299
+ {(products ?? []).length === 0 && (
300
+ <div className="p-10 text-center text-muted-foreground text-sm">
301
+ No products yet. Add your first one above.
302
+ </div>
303
+ )}
304
+ </div>
305
+ </>
306
+ )}
307
+
308
+ {tab === "admins" && <AdminsPanel isMaster={isMaster} />}
309
+
310
+ {editing && (
311
+ <EditDialog
312
+ product={editing}
313
+ categories={categories ?? []}
314
+ onClose={() => setEditing(null)}
315
+ onSave={(d) => upsert.mutate(d)}
316
+ saving={upsert.isPending}
317
+ onImport={(url) => importFn({ data: { url } })}
318
+ importingLabel="Import Product From Link"
319
+ />
320
+ )}
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function AdminsPanel({ isMaster }: { isMaster: boolean }) {
326
+ const qc = useQueryClient();
327
+ const listFn = useServerFn(listAdmins);
328
+ const createFn = useServerFn(createAdmin);
329
+ const { data: admins } = useQuery({ queryKey: ["admins"], queryFn: () => listFn() });
330
+
331
+ const [form, setForm] = useState({ email: "", password: "", admin_id: "" });
332
+
333
+ const create = useMutation({
334
+ mutationFn: (data: typeof form) => createFn({ data }),
335
+ onSuccess: () => {
336
+ toast.success("Admin added");
337
+ qc.invalidateQueries({ queryKey: ["admins"] });
338
+ setForm({ email: "", password: "", admin_id: "" });
339
+ },
340
+ onError: (e: any) => toast.error(e.message ?? "Failed"),
341
+ });
342
+
343
+ return (
344
+ <div className="mt-6 grid lg:grid-cols-2 gap-6">
345
+ <div className="glass-strong rounded-3xl p-6">
346
+ <h2 className="font-display text-xl font-bold inline-flex items-center gap-2">
347
+ <UserPlus className="w-5 h-5 text-brand-magenta" /> Add new admin
348
+ </h2>
349
+ {!isMaster ? (
350
+ <p className="text-sm text-muted-foreground mt-3">
351
+ Only the Master Admin can add new admins. Ask the Master Admin (admin ID{" "}
352
+ <span className="font-mono text-foreground">doraexplora</span>) to assign you an Admin
353
+ ID.
354
+ </p>
355
+ ) : (
356
+ <form
357
+ onSubmit={(e) => {
358
+ e.preventDefault();
359
+ create.mutate(form);
360
+ }}
361
+ className="space-y-3 mt-5"
362
+ >
363
+ <FieldRow
364
+ icon={<KeyRound className="w-4 h-4" />}
365
+ label="Assign Admin ID"
366
+ value={form.admin_id}
367
+ onChange={(v) => setForm((f) => ({ ...f, admin_id: v }))}
368
+ placeholder="e.g. jane_admin"
369
+ />
370
+ <FieldRow
371
+ icon={<Mail className="w-4 h-4" />}
372
+ label="Admin email"
373
+ type="email"
374
+ value={form.email}
375
+ onChange={(v) => setForm((f) => ({ ...f, email: v }))}
376
+ placeholder="admin@company.com"
377
+ />
378
+ <FieldRow
379
+ icon={<Lock className="w-4 h-4" />}
380
+ label="Initial password"
381
+ type="password"
382
+ value={form.password}
383
+ onChange={(v) => setForm((f) => ({ ...f, password: v }))}
384
+ placeholder="Min 8 characters"
385
+ />
386
+ <button
387
+ disabled={create.isPending || !form.email || !form.password || !form.admin_id}
388
+ className="btn-glow w-full rounded-full py-3 font-semibold text-sm disabled:opacity-60"
389
+ >
390
+ {create.isPending ? "Creating…" : "Create admin"}
391
+ </button>
392
+ <p className="text-xs text-muted-foreground pt-1">
393
+ The new admin will sign in at <span className="font-mono">/admin/login</span> using
394
+ this Admin ID, email, and password.
395
+ </p>
396
+ </form>
397
+ )}
398
+ </div>
399
+
400
+ <div className="glass-strong rounded-3xl p-6">
401
+ <h2 className="font-display text-xl font-bold">Current admins</h2>
402
+ <div className="mt-4 divide-y divide-white/5">
403
+ {(admins ?? []).map((a) => (
404
+ <div key={a.user_id} className="py-3 flex items-center justify-between gap-3">
405
+ <div className="min-w-0">
406
+ <div className="text-sm font-medium truncate inline-flex items-center gap-2">
407
+ {a.is_master && <Crown className="w-3.5 h-3.5 text-brand-amber" />}
408
+ {a.email ?? "—"}
409
+ </div>
410
+ <div className="text-xs text-muted-foreground font-mono">
411
+ {a.admin_id ?? "(no admin id)"}
412
+ </div>
413
+ </div>
414
+ {a.is_master && (
415
+ <span className="text-[10px] bg-brand-amber/20 text-brand-amber rounded px-2 py-0.5 uppercase tracking-wider">
416
+ Master
417
+ </span>
418
+ )}
419
+ </div>
420
+ ))}
421
+ {(admins ?? []).length === 0 && (
422
+ <p className="text-sm text-muted-foreground py-4">No admins yet.</p>
423
+ )}
424
+ </div>
425
+ </div>
426
+ </div>
427
+ );
428
+ }
429
+
430
+ function FieldRow({
431
+ icon,
432
+ label,
433
+ value,
434
+ onChange,
435
+ placeholder,
436
+ type = "text",
437
+ }: {
438
+ icon: React.ReactNode;
439
+ label: string;
440
+ value: string;
441
+ onChange: (v: string) => void;
442
+ placeholder?: string;
443
+ type?: string;
444
+ }) {
445
+ return (
446
+ <div>
447
+ <label className="text-xs text-muted-foreground">{label}</label>
448
+ <div className="mt-1 flex items-center gap-2 glass rounded-xl px-3 py-2.5">
449
+ <span className="text-muted-foreground">{icon}</span>
450
+ <input
451
+ type={type}
452
+ value={value}
453
+ onChange={(e) => onChange(e.target.value)}
454
+ placeholder={placeholder}
455
+ className="flex-1 bg-transparent outline-none text-sm placeholder:text-muted-foreground"
456
+ />
457
+ </div>
458
+ </div>
459
+ );
460
+ }
461
+
462
+ function Stat({ label, value, icon }: { label: string; value: any; icon?: React.ReactNode }) {
463
+ return (
464
+ <div className="glass rounded-2xl p-4">
465
+ <div className="text-xs text-muted-foreground inline-flex items-center gap-1">
466
+ {icon}
467
+ {label}
468
+ </div>
469
+ <div className="font-display text-2xl font-bold mt-1">{value}</div>
470
+ </div>
471
+ );
472
+ }
473
+
474
+ function EditDialog({
475
+ product,
476
+ categories,
477
+ onClose,
478
+ onSave,
479
+ saving,
480
+ onImport,
481
+ importingLabel,
482
+ }: {
483
+ product: any;
484
+ categories: any[];
485
+ onClose: () => void;
486
+ onSave: (d: any) => void;
487
+ saving: boolean;
488
+ onImport: (url: string) => Promise<any>;
489
+ importingLabel: string;
490
+ }) {
491
+ const [form, setForm] = useState<any>({
492
+ id: product.id,
493
+ title: product.title ?? "",
494
+ description: product.description ?? "",
495
+ image_url: product.image_url ?? "",
496
+ price: product.price ?? 0,
497
+ original_price: product.original_price ?? null,
498
+ currency: product.currency ?? "USD",
499
+ affiliate_url: product.affiliate_url ?? "",
500
+ product_url: product.product_url ?? "",
501
+ tracking_url: product.tracking_url ?? "",
502
+ source_url: product.source_url ?? "",
503
+ imported_from: product.imported_from ?? "",
504
+ import_notes: product.import_notes ?? "",
505
+ merchant: product.merchant ?? "",
506
+ category_id: product.category_id ?? null,
507
+ tags: product.tags ?? [],
508
+ badges: product.badges ?? [],
509
+ is_trending: product.is_trending ?? false,
510
+ is_hot: product.is_hot ?? false,
511
+ is_featured: product.is_featured ?? false,
512
+ shipping_info: product.shipping_info ?? "",
513
+ delivery_estimate: product.delivery_estimate ?? "",
514
+ });
515
+
516
+ const [importUrl, setImportUrl] = useState(
517
+ product.product_url ?? product.source_url ?? product.affiliate_url ?? "",
518
+ );
519
+ const [importing, setImporting] = useState(false);
520
+ const [importError, setImportError] = useState<string | null>(null);
521
+
522
+ const set = (k: string, v: any) => setForm((f: any) => ({ ...f, [k]: v }));
523
+
524
+ const applyImported = (payload: any) => {
525
+ setForm((current: any) => ({
526
+ ...current,
527
+ title: payload.title ?? current.title,
528
+ description: payload.description ?? current.description,
529
+ image_url: payload.image_url ?? current.image_url,
530
+ price: payload.price ?? current.price,
531
+ original_price: payload.original_price ?? current.original_price,
532
+ currency: payload.currency ?? current.currency,
533
+ merchant: payload.merchant ?? current.merchant,
534
+ product_url: payload.product_url ?? current.product_url,
535
+ tracking_url: payload.tracking_url ?? current.tracking_url,
536
+ source_url: payload.source_url ?? current.source_url,
537
+ imported_from: payload.imported_from ?? current.imported_from,
538
+ import_notes: payload.import_notes ?? current.import_notes,
539
+ tags: payload.tags ?? current.tags,
540
+ badges: payload.badges ?? current.badges,
541
+ shipping_info: payload.shipping_info ?? current.shipping_info,
542
+ delivery_estimate: payload.delivery_estimate ?? current.delivery_estimate,
543
+ affiliate_url: payload.tracking_url ?? payload.product_url ?? current.affiliate_url,
544
+ }));
545
+ if (payload.product_url) setImportUrl(payload.product_url);
546
+ if (payload.import_notes) setImportError(null);
547
+ };
548
+
549
+ return (
550
+ <div
551
+ className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto"
552
+ onClick={onClose}
553
+ >
554
+ <div
555
+ className="glass-strong rounded-3xl p-6 max-w-3xl w-full my-auto"
556
+ onClick={(e) => e.stopPropagation()}
557
+ >
558
+ <div className="flex items-start justify-between gap-4 mb-5">
559
+ <div>
560
+ <h2 className="font-display text-2xl font-bold">
561
+ {product.id ? "Edit product" : "New product"}
562
+ </h2>
563
+ <p className="text-xs text-muted-foreground mt-1 inline-flex items-center gap-1.5">
564
+ <Sparkles className="w-3.5 h-3.5 text-brand-magenta" />
565
+ Keep the shopping experience clean while still allowing tracking behind the scenes.
566
+ </p>
567
+ </div>
568
+ <span className="text-[10px] uppercase tracking-wider bg-white/5 border border-white/10 rounded-full px-3 py-1 text-muted-foreground">
569
+ Manual + import
570
+ </span>
571
+ </div>
572
+
573
+ <div className="rounded-3xl border border-white/10 bg-white/5 p-4 mb-5">
574
+ <div className="flex flex-col md:flex-row gap-3 md:items-end">
575
+ <div className="flex-1">
576
+ <label className="text-xs text-muted-foreground">Import Product From Link</label>
577
+ <div className="mt-1 flex items-center gap-2 glass rounded-xl px-3 py-2.5">
578
+ <Link2 className="w-4 h-4 text-brand-magenta shrink-0" />
579
+ <input
580
+ value={importUrl}
581
+ onChange={(e) => setImportUrl(e.target.value)}
582
+ placeholder="Paste an AliExpress, Temu, Amazon, eBay, Alibaba, or other store link"
583
+ className="flex-1 bg-transparent outline-none text-sm placeholder:text-muted-foreground"
584
+ />
585
+ </div>
586
+ </div>
587
+ <button
588
+ type="button"
589
+ disabled={importing || !importUrl.trim()}
590
+ onClick={async () => {
591
+ try {
592
+ setImporting(true);
593
+ setImportError(null);
594
+ const preview = await onImport(importUrl.trim());
595
+ applyImported(preview);
596
+ toast.success("Imported product details");
597
+ } catch (e: any) {
598
+ const message = e?.message ?? "Import failed";
599
+ setImportError(message);
600
+ toast.error(message);
601
+ } finally {
602
+ setImporting(false);
603
+ }
604
+ }}
605
+ className="btn-glow rounded-full px-5 py-3 font-semibold text-sm inline-flex items-center gap-2 disabled:opacity-60"
606
+ >
607
+ {importing ? (
608
+ <Loader2 className="w-4 h-4 animate-spin" />
609
+ ) : (
610
+ <FileDown className="w-4 h-4" />
611
+ )}
612
+ {importing ? "Fetching…" : importingLabel}
613
+ </button>
614
+ </div>
615
+ {importError ? (
616
+ <div className="mt-3 text-xs text-destructive inline-flex items-center gap-2">
617
+ <RefreshCw className="w-3.5 h-3.5" />
618
+ {importError}
619
+ </div>
620
+ ) : (
621
+ <p className="mt-3 text-xs text-muted-foreground">
622
+ We’ll try to fetch title, price, description, images, ratings, and source details
623
+ automatically. You can edit everything before publishing.
624
+ </p>
625
+ )}
626
+ </div>
627
+
628
+ <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
629
+ <Inp label="Title" value={form.title} onChange={(v) => set("title", v)} />
630
+ <Inp
631
+ label="Description"
632
+ value={form.description}
633
+ onChange={(v) => set("description", v)}
634
+ textarea
635
+ />
636
+ <div className="grid md:grid-cols-2 gap-3">
637
+ <Inp label="Image URL" value={form.image_url} onChange={(v) => set("image_url", v)} />
638
+ <Inp
639
+ label="Product URL"
640
+ value={form.product_url}
641
+ onChange={(v) => set("product_url", v)}
642
+ />
643
+ </div>
644
+ <div className="grid md:grid-cols-2 gap-3">
645
+ <Inp
646
+ label="Tracking URL"
647
+ value={form.tracking_url}
648
+ onChange={(v) => set("tracking_url", v)}
649
+ />
650
+ <Inp
651
+ label="Source URL"
652
+ value={form.source_url}
653
+ onChange={(v) => set("source_url", v)}
654
+ />
655
+ </div>
656
+ <div className="grid md:grid-cols-2 gap-3">
657
+ <Inp
658
+ label="Affiliate URL"
659
+ value={form.affiliate_url}
660
+ onChange={(v) => set("affiliate_url", v)}
661
+ />
662
+ <Inp label="Currency" value={form.currency} onChange={(v) => set("currency", v)} />
663
+ </div>
664
+ <div className="grid grid-cols-2 gap-3">
665
+ <Inp
666
+ label="Price"
667
+ type="number"
668
+ value={form.price}
669
+ onChange={(v) => set("price", Number(v))}
670
+ />
671
+ <Inp
672
+ label="Original price"
673
+ type="number"
674
+ value={form.original_price ?? ""}
675
+ onChange={(v) => set("original_price", v ? Number(v) : null)}
676
+ />
677
+ </div>
678
+ <div className="grid grid-cols-2 gap-3">
679
+ <Inp
680
+ label="Merchant"
681
+ value={form.merchant ?? ""}
682
+ onChange={(v) => set("merchant", v)}
683
+ />
684
+ <div>
685
+ <label className="text-xs text-muted-foreground">Category</label>
686
+ <select
687
+ value={form.category_id ?? ""}
688
+ onChange={(e) => set("category_id", e.target.value || null)}
689
+ className="w-full glass rounded-xl px-3 py-2.5 text-sm bg-transparent outline-none mt-1"
690
+ >
691
+ <option value="">— None —</option>
692
+ {categories.map((c: any) => (
693
+ <option key={c.id} value={c.id} className="bg-background">
694
+ {c.name}
695
+ </option>
696
+ ))}
697
+ </select>
698
+ </div>
699
+ </div>
700
+ <div className="grid md:grid-cols-2 gap-3">
701
+ <Inp
702
+ label="Imported from"
703
+ value={form.imported_from ?? ""}
704
+ onChange={(v) => set("imported_from", v)}
705
+ />
706
+ <Inp
707
+ label="Import notes"
708
+ value={form.import_notes ?? ""}
709
+ onChange={(v) => set("import_notes", v)}
710
+ />
711
+ </div>
712
+ <div className="grid grid-cols-2 gap-3">
713
+ <Inp
714
+ label="Shipping info"
715
+ value={form.shipping_info ?? ""}
716
+ onChange={(v) => set("shipping_info", v)}
717
+ />
718
+ <Inp
719
+ label="Delivery estimate"
720
+ value={form.delivery_estimate ?? ""}
721
+ onChange={(v) => set("delivery_estimate", v)}
722
+ />
723
+ </div>
724
+ <div className="flex flex-wrap gap-4 pt-2">
725
+ <Toggle
726
+ label="Trending"
727
+ checked={form.is_trending}
728
+ onChange={(v) => set("is_trending", v)}
729
+ />
730
+ <Toggle label="Hot deal" checked={form.is_hot} onChange={(v) => set("is_hot", v)} />
731
+ <Toggle
732
+ label="Featured"
733
+ checked={form.is_featured}
734
+ onChange={(v) => set("is_featured", v)}
735
+ />
736
+ </div>
737
+ </div>
738
+
739
+ <div className="flex justify-end gap-2 pt-5 mt-3 border-t border-white/10">
740
+ <button onClick={onClose} className="px-4 py-2 rounded-full text-sm hover:bg-white/5">
741
+ Cancel
742
+ </button>
743
+ <button
744
+ disabled={saving}
745
+ onClick={() => onSave(form)}
746
+ className="btn-glow rounded-full px-5 py-2 font-semibold text-sm disabled:opacity-60"
747
+ >
748
+ {saving ? "Saving…" : "Save"}
749
+ </button>
750
+ </div>
751
+ </div>
752
+ </div>
753
+ );
754
+ }
755
+
756
+ function Inp({
757
+ label,
758
+ value,
759
+ onChange,
760
+ type = "text",
761
+ textarea,
762
+ }: {
763
+ label: string;
764
+ value: any;
765
+ onChange: (v: string) => void;
766
+ type?: string;
767
+ textarea?: boolean;
768
+ }) {
769
+ return (
770
+ <div>
771
+ <label className="text-xs text-muted-foreground">{label}</label>
772
+ {textarea ? (
773
+ <textarea
774
+ value={value}
775
+ onChange={(e) => onChange(e.target.value)}
776
+ rows={3}
777
+ className="w-full glass rounded-xl px-3 py-2 text-sm bg-transparent outline-none mt-1 resize-none"
778
+ />
779
+ ) : (
780
+ <input
781
+ type={type}
782
+ value={value}
783
+ onChange={(e) => onChange(e.target.value)}
784
+ className="w-full glass rounded-xl px-3 py-2 text-sm bg-transparent outline-none mt-1"
785
+ />
786
+ )}
787
+ </div>
788
+ );
789
+ }
790
+
791
+ function Toggle({
792
+ label,
793
+ checked,
794
+ onChange,
795
+ }: {
796
+ label: string;
797
+ checked: boolean;
798
+ onChange: (v: boolean) => void;
799
+ }) {
800
+ return (
801
+ <label className="inline-flex items-center gap-2 text-sm cursor-pointer">
802
+ <input
803
+ type="checkbox"
804
+ checked={checked}
805
+ onChange={(e) => onChange(e.target.checked)}
806
+ className="accent-brand-magenta w-4 h-4"
807
+ />
808
+ {label}
809
+ </label>
810
+ );
811
+ }