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.
- package/.wrangler/deploy/config.json +1 -0
- package/bunfig.toml +6 -0
- package/components.json +22 -0
- package/config.json +0 -0
- package/dist/client/.assetsignore +2 -0
- package/dist/client/assets/ProductCard-DbIkJAE-.js +1 -0
- package/dist/client/assets/about-AskxOruL.js +1 -0
- package/dist/client/assets/admin-BZVcAQM3.js +1 -0
- package/dist/client/assets/admin.functions--RdVcuBx.js +1 -0
- package/dist/client/assets/admin.login-QgrF_9Fp.js +1 -0
- package/dist/client/assets/affiliate-disclosure-BIAsA-HO.js +1 -0
- package/dist/client/assets/categories-D0N418mK.js +1 -0
- package/dist/client/assets/category._slug-aCaQm14E.js +1 -0
- package/dist/client/assets/contact-PhvO-V15.js +1 -0
- package/dist/client/assets/faq-BsiHWPM8.js +1 -0
- package/dist/client/assets/hero-bg-BP2eVUIX.jpg +0 -0
- package/dist/client/assets/index-BU9rnkF3.js +1 -0
- package/dist/client/assets/index-BpJWZkva.js +1 -0
- package/dist/client/assets/index-vRX-zAyq.js +1 -0
- package/dist/client/assets/login-DteE0ZGp.js +1 -0
- package/dist/client/assets/logo-pSNfLJQk.png +0 -0
- package/dist/client/assets/privacy-B_Pu7040.js +1 -0
- package/dist/client/assets/product-links-BkZ41Gv3.js +1 -0
- package/dist/client/assets/product._id-BVUysCW-.js +1 -0
- package/dist/client/assets/products.functions-cGzRziKO.js +1 -0
- package/dist/client/assets/profile-CveRcKq2.js +1 -0
- package/dist/client/assets/reset-password-ySEjItX_.js +1 -0
- package/dist/client/assets/saved-CHtdQDJF.js +1 -0
- package/dist/client/assets/search-CXWfET1y.js +1 -0
- package/dist/client/assets/signup-CEx90iuV.js +1 -0
- package/dist/client/assets/styles-DrNJG0BO.css +1 -0
- package/dist/client/assets/terms-VqJ9kX9b.js +1 -0
- package/dist/client/assets/update-password-C-d0ix5e.js +1 -0
- package/dist/client/assets/vendor-aria-hidden-DvXkyWUv.js +1 -0
- package/dist/client/assets/vendor-class-variance-authority-5VPnzWs2.js +1 -0
- package/dist/client/assets/vendor-clsx-B-dksMZM.js +1 -0
- package/dist/client/assets/vendor-cookie-es-CS0aJGDi.js +1 -0
- package/dist/client/assets/vendor-detect-node-es-l0sNRNKZ.js +1 -0
- package/dist/client/assets/vendor-floating-ui-core-BlUy28sp.js +1 -0
- package/dist/client/assets/vendor-floating-ui-dom-BxK0hn2R.js +1 -0
- package/dist/client/assets/vendor-floating-ui-react-dom-Bas3975S.js +1 -0
- package/dist/client/assets/vendor-floating-ui-utils-BfYUAVcw.js +1 -0
- package/dist/client/assets/vendor-framer-motion-BMdL-cuX.js +9 -0
- package/dist/client/assets/vendor-get-nonce-C-Z93AgS.js +1 -0
- package/dist/client/assets/vendor-iceberg-js-tWD4K6Lg.js +1 -0
- package/dist/client/assets/vendor-lovable.dev-cloud-auth-js-VuzqtJVg.js +1 -0
- package/dist/client/assets/vendor-lucide-react-b5K2fehp.js +1 -0
- package/dist/client/assets/vendor-motion-dom-BETJamZt.js +1 -0
- package/dist/client/assets/vendor-motion-utils-BuWewJbj.js +1 -0
- package/dist/client/assets/vendor-radix-ui-primitive-Dc_FVRD7.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-accordion-C22Rgxe9.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-arrow-DMHj2mKI.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-avatar-CVPBkFXg.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-collapsible-BvM-4sKX.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-collection-D9KtqmHm.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-compose-refs-Cvq0AS8Z.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-context-CAqqn5Nx.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-dialog-DZ01vOLq.js +5 -0
- package/dist/client/assets/vendor-radix-ui-react-direction-DxZwNuei.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-dismissable-layer-Dqgrs55Y.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-dropdown-menu-0uzvrqkn.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-focus-guards-DgWoZ-fP.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-focus-scope-BLIu5QaL.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-id-bpga_rLa.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-menu-D0qf2r6_.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-popper-BafIylxU.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-portal-BnAsfNCS.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-presence-C-f3UKQ2.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-primitive-zTHwXNoz.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-roving-focus-jyJB8K2E.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-slot-6LXHJrHl.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-callback-ref-E91aPc6s.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-controllable-state-Ca3eMtxa.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-effect-event-CPeX4A3c.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-escape-keydown-7n3YsXFo.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-is-hydrated-C1PY1qNv.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-layout-effect-B3AcGWPy.js +1 -0
- package/dist/client/assets/vendor-radix-ui-react-use-size-CXS04sct.js +1 -0
- package/dist/client/assets/vendor-react-dom-BnNs-kzm.js +9 -0
- package/dist/client/assets/vendor-react-gJPiVnX5.js +1 -0
- package/dist/client/assets/vendor-react-remove-scroll-DHKl-IMP.js +4 -0
- package/dist/client/assets/vendor-react-remove-scroll-bar-CSjdInc2.js +38 -0
- package/dist/client/assets/vendor-react-style-singleton-BqHpkgXn.js +1 -0
- package/dist/client/assets/vendor-scheduler-7OC5HNn7.js +1 -0
- package/dist/client/assets/vendor-seroval-B_Fur-nl.js +3 -0
- package/dist/client/assets/vendor-seroval-plugins-CBHnPkZJ.js +1 -0
- package/dist/client/assets/vendor-sonner-71-LdGG1.js +1 -0
- package/dist/client/assets/vendor-supabase-auth-js-DWrN-bIx.js +18 -0
- package/dist/client/assets/vendor-supabase-functions-js-uY_V-TxC.js +1 -0
- package/dist/client/assets/vendor-supabase-phoenix-BzEf37Ve.js +2 -0
- package/dist/client/assets/vendor-supabase-postgrest-js-C4rBWbCx.js +4 -0
- package/dist/client/assets/vendor-supabase-realtime-js-D6BlOYKE.js +23 -0
- package/dist/client/assets/vendor-supabase-storage-js-BG98L3Zz.js +1 -0
- package/dist/client/assets/vendor-supabase-supabase-js-DCCzdwBJ.js +1 -0
- package/dist/client/assets/vendor-tailwind-merge-Ct12j0u0.js +1 -0
- package/dist/client/assets/vendor-tanstack-history-C617CaxG.js +1 -0
- package/dist/client/assets/vendor-tanstack-query-core-7wuJJ5ZL.js +1 -0
- package/dist/client/assets/vendor-tanstack-react-query-HImzo8sX.js +1 -0
- package/dist/client/assets/vendor-tanstack-react-router-sIZLK-LU.js +1 -0
- package/dist/client/assets/vendor-tanstack-react-start-client-GiYCfWmf.js +1 -0
- package/dist/client/assets/vendor-tanstack-react-store-EvTi3ahh.js +1 -0
- package/dist/client/assets/vendor-tanstack-router-core-Cr7bYUZv.js +1 -0
- package/dist/client/assets/vendor-tanstack-start-client-core-C-00BBOu.js +2 -0
- package/dist/client/assets/vendor-tanstack-start-fn-stubs-l0sNRNKZ.js +1 -0
- package/dist/client/assets/vendor-tanstack-store-BC7mA7pq.js +1 -0
- package/dist/client/assets/vendor-tslib-Du-meQkk.js +1 -0
- package/dist/client/assets/vendor-use-callback-ref-C_fIAtot.js +1 -0
- package/dist/client/assets/vendor-use-sidecar-Bh0DDN6h.js +1 -0
- package/dist/client/assets/vendor-use-sync-external-store-ZvKHXaIn.js +1 -0
- package/dist/client/assets/vendor-vercel-analytics-DwPM5BWs.js +1 -0
- package/dist/client/assets/vendor-zod-By9teAtI.js +1 -0
- package/dist/client/robots.txt +2 -0
- package/dist/server/.dev.vars +5 -0
- package/dist/server/.vite/manifest.json +2528 -0
- package/dist/server/assets/ProductCard-CUPXy5Eo.js +149 -0
- package/dist/server/assets/_tanstack-start-manifest_v-do7vTWFD.js +4 -0
- package/dist/server/assets/about-TfKQw0Ga.js +28 -0
- package/dist/server/assets/admin-DspfJOJk.js +578 -0
- package/dist/server/assets/admin.functions-B78ppWLR.js +645 -0
- package/dist/server/assets/admin.functions-BWlKBoTL.js +93 -0
- package/dist/server/assets/admin.login-CV7QfeA6.js +139 -0
- package/dist/server/assets/affiliate-disclosure-B1wI1cDb.js +86 -0
- package/dist/server/assets/auth-middleware-Cn49MidW.js +62 -0
- package/dist/server/assets/categories-Z7jnAYZP.js +108 -0
- package/dist/server/assets/category._slug-D0XY3FGK.js +112 -0
- package/dist/server/assets/contact-IzyONsXs.js +104 -0
- package/dist/server/assets/faq-aRhB_CR3.js +133 -0
- package/dist/server/assets/hero-bg-BP2eVUIX.jpg +0 -0
- package/dist/server/assets/index-BTPHbXw9.js +221 -0
- package/dist/server/assets/index-ByJkHkrU.js +30 -0
- package/dist/server/assets/login-Dvy5Dm0f.js +175 -0
- package/dist/server/assets/logo-pSNfLJQk.png +0 -0
- package/dist/server/assets/privacy-B6Wiez1P.js +93 -0
- package/dist/server/assets/product-links-CGYEPP56.js +16 -0
- package/dist/server/assets/product._id-BpRa-1z0.js +231 -0
- package/dist/server/assets/products.functions-DSlmibYN.js +209 -0
- package/dist/server/assets/products.functions-DlHkRiqi.js +24 -0
- package/dist/server/assets/profile-B0NWzVAZ.js +314 -0
- package/dist/server/assets/reset-password-CY-rmqMr.js +115 -0
- package/dist/server/assets/saved-7FA6Dbom.js +126 -0
- package/dist/server/assets/search-Yw5c_fZa.js +329 -0
- package/dist/server/assets/signup-UPzgZo4i.js +143 -0
- package/dist/server/assets/styles-DrNJG0BO.css +1 -0
- package/dist/server/assets/terms-CMnX95bP.js +89 -0
- package/dist/server/assets/update-password-Cr94ea8n.js +131 -0
- package/dist/server/assets/vendor-aria-hidden-DPa16MWu.js +122 -0
- package/dist/server/assets/vendor-class-variance-authority-0YxJPB9Y.js +44 -0
- package/dist/server/assets/vendor-cloudflare-unenv-preset-ya0VEFBz.js +250 -0
- package/dist/server/assets/vendor-clsx-DgYk2OaC.js +16 -0
- package/dist/server/assets/vendor-cookie-es-DAoofYiI.js +44 -0
- package/dist/server/assets/vendor-detect-node-es-l0sNRNKZ.js +1 -0
- package/dist/server/assets/vendor-floating-ui-core-3tkK0THV.js +726 -0
- package/dist/server/assets/vendor-floating-ui-dom-C-cPtgJv.js +626 -0
- package/dist/server/assets/vendor-floating-ui-react-dom-CRG6gBpH.js +319 -0
- package/dist/server/assets/vendor-floating-ui-utils-DmXANH-E.js +320 -0
- package/dist/server/assets/vendor-framer-motion-X4zAkX3J.js +1979 -0
- package/dist/server/assets/vendor-get-nonce-DiSj3EHl.js +9 -0
- package/dist/server/assets/vendor-h3-v2-CCobnLY5.js +287 -0
- package/dist/server/assets/vendor-iceberg-js-bHCkXyJn.js +534 -0
- package/dist/server/assets/vendor-isbot-CZ7WjwVs.js +21 -0
- package/dist/server/assets/vendor-lovable.dev-cloud-auth-js-BE03njZw.js +180 -0
- package/dist/server/assets/vendor-lucide-react-Ddew6HYb.js +458 -0
- package/dist/server/assets/vendor-motion-dom-D2MTwGIG.js +5983 -0
- package/dist/server/assets/vendor-motion-utils-LJlIFN6m.js +161 -0
- package/dist/server/assets/vendor-radix-ui-primitive-B-mNdDrH.js +11 -0
- package/dist/server/assets/vendor-radix-ui-react-accordion-1Izf6x00.js +308 -0
- package/dist/server/assets/vendor-radix-ui-react-arrow-B882lnFK.js +23 -0
- package/dist/server/assets/vendor-radix-ui-react-avatar-BVgZt2Ab.js +209 -0
- package/dist/server/assets/vendor-radix-ui-react-collapsible-DCBbMZiS.js +147 -0
- package/dist/server/assets/vendor-radix-ui-react-collection-BZ2srfgU.js +150 -0
- package/dist/server/assets/vendor-radix-ui-react-compose-refs-D3qsKVk1.js +39 -0
- package/dist/server/assets/vendor-radix-ui-react-context-BVoNDLue.js +78 -0
- package/dist/server/assets/vendor-radix-ui-react-dialog-DlxMaNYK.js +406 -0
- package/dist/server/assets/vendor-radix-ui-react-direction-Dt_WDL1t.js +9 -0
- package/dist/server/assets/vendor-radix-ui-react-dismissable-layer-CjsuPohV.js +210 -0
- package/dist/server/assets/vendor-radix-ui-react-dropdown-menu-DVxKumY8.js +263 -0
- package/dist/server/assets/vendor-radix-ui-react-focus-guards-D_6NoePE.js +29 -0
- package/dist/server/assets/vendor-radix-ui-react-focus-scope-DEIhTJJH.js +206 -0
- package/dist/server/assets/vendor-radix-ui-react-id-DFFpgh6m.js +14 -0
- package/dist/server/assets/vendor-radix-ui-react-menu-CiTMLwjT.js +893 -0
- package/dist/server/assets/vendor-radix-ui-react-popper-23Ye2Vyc.js +286 -0
- package/dist/server/assets/vendor-radix-ui-react-portal-CZCH5uPk.js +16 -0
- package/dist/server/assets/vendor-radix-ui-react-presence-CaAULlDU.js +128 -0
- package/dist/server/assets/vendor-radix-ui-react-primitive-BeOk3UYa.js +124 -0
- package/dist/server/assets/vendor-radix-ui-react-roving-focus-DES9GR8l.js +224 -0
- package/dist/server/assets/vendor-radix-ui-react-slot-DUhZbzoH.js +103 -0
- package/dist/server/assets/vendor-radix-ui-react-use-callback-ref-BynBgohw.js +11 -0
- package/dist/server/assets/vendor-radix-ui-react-use-controllable-state-C9KpT6DG.js +69 -0
- package/dist/server/assets/vendor-radix-ui-react-use-effect-event-gpNY2xjS.js +1 -0
- package/dist/server/assets/vendor-radix-ui-react-use-escape-keydown-CcYRQ2pp.js +17 -0
- package/dist/server/assets/vendor-radix-ui-react-use-is-hydrated-D_LcBPXY.js +15 -0
- package/dist/server/assets/vendor-radix-ui-react-use-layout-effect-1LNLXAjr.js +6 -0
- package/dist/server/assets/vendor-radix-ui-react-use-size-D6fiKJQo.js +39 -0
- package/dist/server/assets/vendor-react-DvBrY0qp.js +511 -0
- package/dist/server/assets/vendor-react-dom-yvMLPM0j.js +10484 -0
- package/dist/server/assets/vendor-react-remove-scroll-BNtiEvVN.js +328 -0
- package/dist/server/assets/vendor-react-remove-scroll-bar-hLqRASRk.js +82 -0
- package/dist/server/assets/vendor-react-style-singleton-BXjcXskB.js +69 -0
- package/dist/server/assets/vendor-rou3-3NaGPdI8.js +8 -0
- package/dist/server/assets/vendor-seroval-dJyC-Zhz.js +1775 -0
- package/dist/server/assets/vendor-seroval-plugins-Pq_U2meB.js +58 -0
- package/dist/server/assets/vendor-sonner-CqbjhsRh.js +1086 -0
- package/dist/server/assets/vendor-srvx-BA-baEX9.js +6 -0
- package/dist/server/assets/vendor-supabase-auth-js-D4xjVprw.js +7602 -0
- package/dist/server/assets/vendor-supabase-functions-js-sWy4UYn1.js +322 -0
- package/dist/server/assets/vendor-supabase-phoenix-Bw3Uh2Nn.js +1777 -0
- package/dist/server/assets/vendor-supabase-postgrest-js-AO-BXa7I.js +4938 -0
- package/dist/server/assets/vendor-supabase-realtime-js-BtdNgJbm.js +2111 -0
- package/dist/server/assets/vendor-supabase-storage-js-Dk_MrPYO.js +2679 -0
- package/dist/server/assets/vendor-supabase-supabase-js-D1EEtG3j.js +697 -0
- package/dist/server/assets/vendor-tailwind-merge-BHb_obmC.js +3255 -0
- package/dist/server/assets/vendor-tanstack-history-C4pKJmkt.js +204 -0
- package/dist/server/assets/vendor-tanstack-query-core-PwwTR5ld.js +2552 -0
- package/dist/server/assets/vendor-tanstack-react-query-hhHzXAK1.js +190 -0
- package/dist/server/assets/vendor-tanstack-react-router-XzqpA65A.js +1120 -0
- package/dist/server/assets/vendor-tanstack-react-start-RvWUpvat.js +37 -0
- package/dist/server/assets/vendor-tanstack-react-start-client-gpNY2xjS.js +1 -0
- package/dist/server/assets/vendor-tanstack-react-start-server-uj_Y9pEN.js +15 -0
- package/dist/server/assets/vendor-tanstack-react-store-gpNY2xjS.js +1 -0
- package/dist/server/assets/vendor-tanstack-router-core-6wywV3KN.js +4252 -0
- package/dist/server/assets/vendor-tanstack-start-client-core-DoOKV2pA.js +1741 -0
- package/dist/server/assets/vendor-tanstack-start-fn-stubs-l0sNRNKZ.js +1 -0
- package/dist/server/assets/vendor-tanstack-start-server-core-CsAstXv7.js +1421 -0
- package/dist/server/assets/vendor-tanstack-start-storage-context-DgH9hIJT.js +17 -0
- package/dist/server/assets/vendor-tanstack-store-l0sNRNKZ.js +1 -0
- package/dist/server/assets/vendor-tslib-_8ICaZ64.js +67 -0
- package/dist/server/assets/vendor-unenv-DUvF4YIF.js +544 -0
- package/dist/server/assets/vendor-use-callback-ref-DMFDRvmi.js +66 -0
- package/dist/server/assets/vendor-use-sidecar-DG1tHua4.js +106 -0
- package/dist/server/assets/vendor-use-sync-external-store-rZ8vi0It.js +64 -0
- package/dist/server/assets/vendor-vercel-analytics-oP8BDp0L.js +168 -0
- package/dist/server/assets/vendor-zod-BRyQdbC-.js +3580 -0
- package/dist/server/index.js +158 -0
- package/dist/server/wrangler.json +1 -0
- package/enable-powershell.ps1 +7 -0
- package/eslint.config.js +41 -0
- package/lint.bat +4 -0
- package/package.json +95 -0
- package/public/robots.txt +2 -0
- package/run-npm-build.cjs +20 -0
- package/run-npm-build.js +20 -0
- package/src/assets/hero-bg.jpg +0 -0
- package/src/assets/logo.png +0 -0
- package/src/components/scrollsy/Footer.tsx +68 -0
- package/src/components/scrollsy/LiveTicker.tsx +31 -0
- package/src/components/scrollsy/Logo.tsx +28 -0
- package/src/components/scrollsy/Nav.tsx +255 -0
- package/src/components/scrollsy/ProductCard.tsx +190 -0
- package/src/components/scrollsy/ProductFilters.tsx +226 -0
- package/src/components/scrollsy/SupportWidget.tsx +197 -0
- package/src/components/ui/accordion.tsx +51 -0
- package/src/components/ui/alert-dialog.tsx +115 -0
- package/src/components/ui/alert.tsx +49 -0
- package/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +32 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +49 -0
- package/src/components/ui/calendar.tsx +177 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +331 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/command.tsx +143 -0
- package/src/components/ui/context-menu.tsx +187 -0
- package/src/components/ui/dialog.tsx +104 -0
- package/src/components/ui/drawer.tsx +98 -0
- package/src/components/ui/dropdown-menu.tsx +188 -0
- package/src/components/ui/form.tsx +171 -0
- package/src/components/ui/hover-card.tsx +27 -0
- package/src/components/ui/input-otp.tsx +69 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/menubar.tsx +229 -0
- package/src/components/ui/navigation-menu.tsx +120 -0
- package/src/components/ui/pagination.tsx +98 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +25 -0
- package/src/components/ui/radio-group.tsx +36 -0
- package/src/components/ui/resizable.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +44 -0
- package/src/components/ui/select.tsx +152 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +122 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +94 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toggle-group.tsx +57 -0
- package/src/components/ui/toggle.tsx +42 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/hooks/use-auth.ts +26 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/integrations/lovable/index.ts +41 -0
- package/src/lib/admin.functions.ts +564 -0
- package/src/lib/error-capture.ts +27 -0
- package/src/lib/error-page.ts +30 -0
- package/src/lib/product-links.ts +39 -0
- package/src/lib/products.functions.ts +101 -0
- package/src/lib/utils.ts +6 -0
- package/src/routeTree.gen.ts +480 -0
- package/src/router.tsx +16 -0
- package/src/routes/__root.tsx +177 -0
- package/src/routes/about.tsx +66 -0
- package/src/routes/admin.login.tsx +95 -0
- package/src/routes/admin.tsx +811 -0
- package/src/routes/affiliate-disclosure.tsx +35 -0
- package/src/routes/categories.tsx +57 -0
- package/src/routes/category.$slug.tsx +51 -0
- package/src/routes/contact.tsx +69 -0
- package/src/routes/faq.tsx +63 -0
- package/src/routes/index.tsx +269 -0
- package/src/routes/login.tsx +160 -0
- package/src/routes/privacy.tsx +39 -0
- package/src/routes/product.$id.tsx +212 -0
- package/src/routes/profile.tsx +393 -0
- package/src/routes/reset-password.tsx +71 -0
- package/src/routes/saved.tsx +83 -0
- package/src/routes/search.tsx +136 -0
- package/src/routes/signup.tsx +108 -0
- package/src/routes/sitemap[.]xml.ts +34 -0
- package/src/routes/terms.tsx +40 -0
- package/src/routes/update-password.tsx +91 -0
- package/src/server.ts +80 -0
- package/src/start.ts +24 -0
- package/src/styles.css +333 -0
- package/terminal-test-output.txt +1 -0
- package/tsconfig.json +27 -0
- package/vercel.json +26 -0
- package/vite.config.ts +38 -0
- 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
|
+
}
|