@unbox-plus/cli 0.20.4
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/LICENSE.md +46 -0
- package/README.md +175 -0
- package/bin/cli.js +439 -0
- package/package.json +43 -0
- package/src/colors.js +100 -0
- package/src/presets.js +241 -0
- package/src/theme.js +253 -0
- package/template/.claude/agents/assets-marca.md +40 -0
- package/template/.claude/agents/avaliador-visual.md +48 -0
- package/template/.claude/agents/branding-briefing.md +401 -0
- package/template/.claude/agents/conteudo-secao.md +46 -0
- package/template/.claude/agents/paginas-legais.md +44 -0
- package/template/.claude/settings.json +3 -0
- package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +55 -0
- package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
- package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
- package/template/.env.example +158 -0
- package/template/.mcp.json.example +14 -0
- package/template/.nvmrc +1 -0
- package/template/CLAUDE.md +333 -0
- package/template/COVERAGE.md +80 -0
- package/template/DEPLOY.md +179 -0
- package/template/QA.md +108 -0
- package/template/README.md +152 -0
- package/template/agents/CONSTRUCAO.md +207 -0
- package/template/agents/MANAGER.md +192 -0
- package/template/agents/PADROES.md +255 -0
- package/template/agents/definitions/00-scaffold.md +56 -0
- package/template/agents/definitions/01-layout.md +47 -0
- package/template/agents/definitions/02-homepage.md +70 -0
- package/template/agents/definitions/03-catalog.md +86 -0
- package/template/agents/definitions/04-pdp.md +97 -0
- package/template/agents/definitions/05-cart.md +116 -0
- package/template/agents/definitions/06-checkout.md +146 -0
- package/template/agents/definitions/07-auth.md +130 -0
- package/template/agents/definitions/08-customer.md +55 -0
- package/template/agents/definitions/09-promotions.md +62 -0
- package/template/agents/definitions/10-feedback.md +128 -0
- package/template/agents/definitions/11-seo-infra.md +159 -0
- package/template/agents/definitions/12-deploy.md +173 -0
- package/template/agents/definitions/13-cro.md +275 -0
- package/template/agents/definitions/14-seo.md +388 -0
- package/template/agents/definitions/15-branding.md +26 -0
- package/template/agents/definitions/16-qa-visual.md +138 -0
- package/template/agents/definitions/17-aeo.md +257 -0
- package/template/app/(loja)/busca/page.tsx +72 -0
- package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
- package/template/app/(loja)/carrinho/page.tsx +160 -0
- package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
- package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
- package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
- package/template/app/(loja)/checkout/page.tsx +80 -0
- package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
- package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
- package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
- package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
- package/template/app/(loja)/conta/entrar/page.tsx +119 -0
- package/template/app/(loja)/conta/page.tsx +45 -0
- package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
- package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
- package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
- package/template/app/(loja)/devolucoes/page.tsx +36 -0
- package/template/app/(loja)/layout.tsx +22 -0
- package/template/app/(loja)/oferta/page.tsx +57 -0
- package/template/app/(loja)/page.tsx +86 -0
- package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
- package/template/app/(loja)/privacidade/page.tsx +113 -0
- package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
- package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
- package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
- package/template/app/(loja)/produtos/loading.tsx +16 -0
- package/template/app/(loja)/produtos/page.tsx +33 -0
- package/template/app/(loja)/termos/page.tsx +104 -0
- package/template/app/acesso/page.tsx +129 -0
- package/template/app/api/account/addresses/route.ts +39 -0
- package/template/app/api/account/exists/route.ts +21 -0
- package/template/app/api/account/me/route.ts +25 -0
- package/template/app/api/account/otp/route.ts +26 -0
- package/template/app/api/account/preferences/route.ts +19 -0
- package/template/app/api/account/signin/route.ts +27 -0
- package/template/app/api/account/signout/route.ts +9 -0
- package/template/app/api/acesso/route.ts +171 -0
- package/template/app/api/capi/route.ts +34 -0
- package/template/app/api/cart/coupon/route.ts +49 -0
- package/template/app/api/cart/items/route.ts +45 -0
- package/template/app/api/cart/link/route.ts +69 -0
- package/template/app/api/cart/route.ts +113 -0
- package/template/app/api/cart/share/route.ts +21 -0
- package/template/app/api/cep/[code]/route.ts +18 -0
- package/template/app/api/checkout/address/route.ts +24 -0
- package/template/app/api/checkout/email/route.ts +41 -0
- package/template/app/api/checkout/installments/route.ts +16 -0
- package/template/app/api/checkout/route.ts +110 -0
- package/template/app/api/checkout/shipping/route.ts +65 -0
- package/template/app/api/checkout-destination/route.ts +31 -0
- package/template/app/api/order/[ref]/route.ts +17 -0
- package/template/app/api/payment-link/route.ts +28 -0
- package/template/app/api/revalidate/route.ts +52 -0
- package/template/app/api/shipping/quote/route.ts +29 -0
- package/template/app/api/subscriptions/[id]/route.ts +52 -0
- package/template/app/api/track/route.ts +28 -0
- package/template/app/api/unbox/catalogo/route.ts +136 -0
- package/template/app/api/unbox/paginas/route.ts +167 -0
- package/template/app/api/unbox/vitrine/route.ts +84 -0
- package/template/app/api/webhooks/unbox/route.ts +97 -0
- package/template/app/apple-icon.svg +5 -0
- package/template/app/error.tsx +15 -0
- package/template/app/globals.css +457 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +90 -0
- package/template/app/llms.txt/route.ts +43 -0
- package/template/app/manifest.ts +21 -0
- package/template/app/not-found.tsx +18 -0
- package/template/app/opengraph-image.tsx +35 -0
- package/template/app/robots.ts +31 -0
- package/template/app/sitemap.ts +42 -0
- package/template/bootstrap.sh +87 -0
- package/template/components/account/account-shell.tsx +47 -0
- package/template/components/account/address-book.tsx +147 -0
- package/template/components/account/preferences-form.tsx +77 -0
- package/template/components/account/reorder-button.tsx +47 -0
- package/template/components/account/signout-button.tsx +20 -0
- package/template/components/account/subscription-actions.tsx +204 -0
- package/template/components/account-nav.tsx +41 -0
- package/template/components/address-fields.tsx +102 -0
- package/template/components/analytics/data-layer-ready.tsx +16 -0
- package/template/components/analytics/purchase-tracker.tsx +32 -0
- package/template/components/brand-search.tsx +34 -0
- package/template/components/cart/cart-provider.tsx +340 -0
- package/template/components/cart/mini-cart.tsx +228 -0
- package/template/components/cart-button.tsx +35 -0
- package/template/components/catalog/catalog-client.tsx +523 -0
- package/template/components/catalog/grid-skeleton.tsx +19 -0
- package/template/components/catalog/pager.tsx +66 -0
- package/template/components/catalog/product-grid-card.tsx +52 -0
- package/template/components/catalog/product-grid.tsx +27 -0
- package/template/components/checkout/checkout-client.tsx +1333 -0
- package/template/components/chrome/announce-bar.tsx +57 -0
- package/template/components/chrome/chrome-recipe.ts +13 -0
- package/template/components/chrome/footers/captura-botao.tsx +28 -0
- package/template/components/chrome/footers/colunas.tsx +110 -0
- package/template/components/chrome/footers/conversao.tsx +90 -0
- package/template/components/chrome/footers/editorial.tsx +98 -0
- package/template/components/chrome/footers/minimal.tsx +55 -0
- package/template/components/chrome/header-bar-mobile.tsx +64 -0
- package/template/components/chrome/headers/centralizado.tsx +71 -0
- package/template/components/chrome/headers/classico.tsx +64 -0
- package/template/components/chrome/headers/compacto.tsx +66 -0
- package/template/components/chrome/headers/equilibrado.tsx +73 -0
- package/template/components/chrome/headers/imersivo.tsx +62 -0
- package/template/components/chrome/registry.ts +70 -0
- package/template/components/chrome/solid-on-scroll.tsx +39 -0
- package/template/components/empty-state.tsx +22 -0
- package/template/components/home/combo-card-compact.tsx +67 -0
- package/template/components/home/combos-home.tsx +64 -0
- package/template/components/home/combos-section.tsx +177 -0
- package/template/components/home/home-recipe.ts +41 -0
- package/template/components/home/sections/attributes-marquee.tsx +178 -0
- package/template/components/home/sections/benefits.tsx +80 -0
- package/template/components/home/sections/bloco-html.tsx +47 -0
- package/template/components/home/sections/catalogo.ts +165 -0
- package/template/components/home/sections/category-pills.tsx +32 -0
- package/template/components/home/sections/combos-carousel.tsx +141 -0
- package/template/components/home/sections/comparison.tsx +73 -0
- package/template/components/home/sections/founder-story.tsx +40 -0
- package/template/components/home/sections/hero.tsx +187 -0
- package/template/components/home/sections/kits.tsx +11 -0
- package/template/components/home/sections/media-cards.tsx +70 -0
- package/template/components/home/sections/newsletter.tsx +62 -0
- package/template/components/home/sections/product-showcase.tsx +175 -0
- package/template/components/home/sections/purchase-hero.tsx +187 -0
- package/template/components/home/sections/quote-banner.tsx +50 -0
- package/template/components/home/sections/registry.ts +186 -0
- package/template/components/home/sections/reviews-carousel.tsx +66 -0
- package/template/components/home/sections/reviews.tsx +68 -0
- package/template/components/home/sections/ritual.tsx +96 -0
- package/template/components/home/sections/savings.tsx +47 -0
- package/template/components/home/sections/social-row.tsx +65 -0
- package/template/components/home/sections/spec-table.tsx +117 -0
- package/template/components/home/sections/stars.tsx +12 -0
- package/template/components/home/sections/stats-grid.tsx +54 -0
- package/template/components/home/sections/trust-bar.tsx +56 -0
- package/template/components/home/sections/trust-strip.tsx +40 -0
- package/template/components/home/sections/video-wall.tsx +59 -0
- package/template/components/landing/landing-recipe.ts +20 -0
- package/template/components/landing/oferta-sections.tsx +40 -0
- package/template/components/landing/product-picker.tsx +257 -0
- package/template/components/mobile-nav.tsx +71 -0
- package/template/components/order-status.tsx +163 -0
- package/template/components/powered-by-unbox.tsx +27 -0
- package/template/components/product/pdp/buy-box.tsx +604 -0
- package/template/components/product/pdp/catalog-grid.tsx +113 -0
- package/template/components/product/pdp/faq-modelo.ts +86 -0
- package/template/components/product/pdp/gallery.tsx +87 -0
- package/template/components/product/pdp/interactive.tsx +200 -0
- package/template/components/product/pdp/newsletter.tsx +64 -0
- package/template/components/product/pdp/payment-chips.tsx +22 -0
- package/template/components/product/pdp/pdp-view.tsx +227 -0
- package/template/components/product/pdp/recommendations.tsx +144 -0
- package/template/components/product/pdp/sections.tsx +173 -0
- package/template/components/quantity-stepper.tsx +36 -0
- package/template/components/search-box.tsx +37 -0
- package/template/components/site-footer.tsx +59 -0
- package/template/components/site-header.tsx +64 -0
- package/template/components/ui/accordion.tsx +72 -0
- package/template/components/ui/alert-dialog.tsx +187 -0
- package/template/components/ui/alert.tsx +76 -0
- package/template/components/ui/aspect-ratio.tsx +22 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +52 -0
- package/template/components/ui/breadcrumb.tsx +125 -0
- package/template/components/ui/button.tsx +58 -0
- package/template/components/ui/card.tsx +103 -0
- package/template/components/ui/carousel.tsx +242 -0
- package/template/components/ui/command.tsx +196 -0
- package/template/components/ui/dialog.tsx +160 -0
- package/template/components/ui/dropdown-menu.tsx +268 -0
- package/template/components/ui/foto.tsx +54 -0
- package/template/components/ui/input-group.tsx +158 -0
- package/template/components/ui/input-otp.tsx +87 -0
- package/template/components/ui/input.tsx +20 -0
- package/template/components/ui/label.tsx +20 -0
- package/template/components/ui/navigation-menu.tsx +168 -0
- package/template/components/ui/pagination.tsx +132 -0
- package/template/components/ui/popover.tsx +90 -0
- package/template/components/ui/progress.tsx +83 -0
- package/template/components/ui/radio-group.tsx +38 -0
- package/template/components/ui/scroll-area.tsx +55 -0
- package/template/components/ui/select.tsx +201 -0
- package/template/components/ui/separator.tsx +25 -0
- package/template/components/ui/sheet.tsx +138 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/sonner.tsx +44 -0
- package/template/components/ui/switch.tsx +32 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +82 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +89 -0
- package/template/components/ui/toggle.tsx +45 -0
- package/template/components/ui/tooltip.tsx +66 -0
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +36 -0
- package/template/gitignore +21 -0
- package/template/lib/analytics.ts +369 -0
- package/template/lib/api.ts +60 -0
- package/template/lib/capi.ts +87 -0
- package/template/lib/cart-link.ts +51 -0
- package/template/lib/cart-normalize.ts +142 -0
- package/template/lib/cart-recovery.ts +23 -0
- package/template/lib/cart-response.ts +48 -0
- package/template/lib/catalog-map.ts +54 -0
- package/template/lib/catalog.ts +3 -0
- package/template/lib/checkout-lock.ts +21 -0
- package/template/lib/checkout-nav.ts +22 -0
- package/template/lib/config.ts +51 -0
- package/template/lib/crm.ts +40 -0
- package/template/lib/customer-session.ts +21 -0
- package/template/lib/dataloader.ts +23 -0
- package/template/lib/editable/config.ts +11 -0
- package/template/lib/editable/document.ts +1762 -0
- package/template/lib/editable/index.ts +3 -0
- package/template/lib/editable/primitives.tsx +821 -0
- package/template/lib/editable/provider.tsx +861 -0
- package/template/lib/editable/rastreio-navegacao.tsx +65 -0
- package/template/lib/editable/rastreio.tsx +168 -0
- package/template/lib/editable/server.ts +189 -0
- package/template/lib/editable/tokens.ts +22 -0
- package/template/lib/editable/verify.ts +24 -0
- package/template/lib/enrichment/combos.ts +184 -0
- package/template/lib/enrichment/index.ts +188 -0
- package/template/lib/enrichment/products.json +1 -0
- package/template/lib/env-check.ts +47 -0
- package/template/lib/format.ts +130 -0
- package/template/lib/icons.ts +67 -0
- package/template/lib/json-ld.ts +9 -0
- package/template/lib/llms-txt.ts +75 -0
- package/template/lib/mockup.ts +24 -0
- package/template/lib/newsletter.ts +12 -0
- package/template/lib/orders.ts +160 -0
- package/template/lib/queries.ts +66 -0
- package/template/lib/ratelimit.ts +47 -0
- package/template/lib/rotas-editaveis.ts +192 -0
- package/template/lib/sanitize.ts +36 -0
- package/template/lib/schemas.ts +78 -0
- package/template/lib/session.ts +98 -0
- package/template/lib/store-config.ts +67 -0
- package/template/lib/unbox/client.ts +829 -0
- package/template/lib/unbox/customer.ts +224 -0
- package/template/lib/unbox/errors.ts +97 -0
- package/template/lib/unbox/index.ts +10 -0
- package/template/lib/unbox/store.ts +113 -0
- package/template/lib/unbox/types.ts +195 -0
- package/template/lib/unbox/webhooks.ts +66 -0
- package/template/lib/utils.ts +6 -0
- package/template/lib/vitrine.ts +242 -0
- package/template/lib/webhook-store.ts +22 -0
- package/template/middleware.ts +178 -0
- package/template/next.config.ts +63 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +5 -0
- package/template/public/brand/coll/default.png +0 -0
- package/template/public/brand/hero-desktop.svg +12 -0
- package/template/public/brand/hero-mobile.svg +12 -0
- package/template/public/brand/heros/boutique-desktop.svg +11 -0
- package/template/public/brand/heros/boutique-mobile.svg +11 -0
- package/template/public/brand/heros/editorial-desktop.svg +11 -0
- package/template/public/brand/heros/editorial-mobile.svg +11 -0
- package/template/public/brand/heros/essencial-desktop.svg +12 -0
- package/template/public/brand/heros/essencial-mobile.svg +12 -0
- package/template/public/brand/heros/promocional-desktop.svg +12 -0
- package/template/public/brand/heros/promocional-mobile.svg +12 -0
- package/template/public/brand/logo-chrome.svg +6 -0
- package/template/public/brand/logo-white.svg +6 -0
- package/template/public/brand/logo.svg +7 -0
- package/template/public/brand/pay-amex.webp +0 -0
- package/template/public/brand/pay-elo.webp +0 -0
- package/template/public/brand/pay-mastercard.webp +0 -0
- package/template/public/brand/pay-pix.webp +0 -0
- package/template/public/brand/pay-visa.webp +0 -0
- package/template/public/brand/ph/avatar-a.svg +5 -0
- package/template/public/brand/ph/avatar-b.svg +5 -0
- package/template/public/brand/ph/avatar-c.svg +5 -0
- package/template/public/brand/ph/avatar-d.svg +5 -0
- package/template/public/brand/ph/photo-a.svg +7 -0
- package/template/public/brand/ph/photo-b.svg +6 -0
- package/template/public/brand/ph/photo-c.svg +6 -0
- package/template/public/brand/ph/poster-a.svg +6 -0
- package/template/public/brand/ph/poster-b.svg +6 -0
- package/template/public/brand/ph/poster-c.svg +6 -0
- package/template/public/unbox/powered-by-fundo-preto.png +0 -0
- package/template/public/unbox/powered-by-transparente.png +0 -0
- package/template/public/unbox/powered-by.png +0 -0
- package/template/scripts/abandoned-cart.ts +82 -0
- package/template/scripts/check-editable.mjs +467 -0
- package/template/scripts/check-honestidade.mjs +111 -0
- package/template/scripts/check-placeholder.mjs +200 -0
- package/template/scripts/check-recipe.mjs +76 -0
- package/template/scripts/check-unbox-brand.mjs +298 -0
- package/template/scripts/contraste.js +177 -0
- package/template/scripts/dump-catalog.ts +95 -0
- package/template/scripts/load-env.ts +13 -0
- package/template/scripts/medir-sistema.mjs +56 -0
- package/template/scripts/place-order-pix.ts +61 -0
- package/template/scripts/qa-screenshots.mjs +150 -0
- package/template/scripts/sistema.py +231 -0
- package/template/scripts/subscribe-webhook.ts +36 -0
- package/template/scripts/test-live.ts +186 -0
- package/template/scripts/vocabulario.py +264 -0
- package/template/tsconfig.json +27 -0
- package/tools/LEIA-ME.md +39 -0
- package/tools/check-template-neutro.mjs +111 -0
- package/tools/notion-doc.mjs +191 -0
- package/tools/tokenize-neutrals.mjs +0 -0
- package/tools/tokenize-radius.mjs +52 -0
- package/tools/workflow-storefront.legado.js +1207 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { getCatalog, getPaymentMethods, getShopData } from "@/lib/queries";
|
|
4
|
+
import { getCartRef, getRecurFreq } from "@/lib/session";
|
|
5
|
+
import { resolveProductPrice } from "@/lib/format";
|
|
6
|
+
import { CheckoutClient, type UpsellOffer } from "@/components/checkout/checkout-client";
|
|
7
|
+
import { DataLayerReady } from "@/components/analytics/data-layer-ready";
|
|
8
|
+
import { mockupOr } from "@/lib/mockup";
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = { title: "Checkout", robots: { index: false } };
|
|
11
|
+
|
|
12
|
+
export default async function CheckoutPage({
|
|
13
|
+
searchParams,
|
|
14
|
+
}: {
|
|
15
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
16
|
+
}) {
|
|
17
|
+
// GARANTIA: a URL do checkout SEMPRE carrega ?id=&token= do carrinho (contrato do link de
|
|
18
|
+
// recuperação — igual ao checkout hospedado da Unbox). Quem chega sem os params (refresh,
|
|
19
|
+
// link direto, navegação antiga) é redirecionado pra URL enriquecida, lida do cookie.
|
|
20
|
+
// Sem isso, CRM que captura a URL navegada (pixel/GTM) não tem como montar a recuperação.
|
|
21
|
+
const sp = await searchParams;
|
|
22
|
+
if (!sp.id || !sp.token) {
|
|
23
|
+
const ref = await getCartRef();
|
|
24
|
+
if (ref) {
|
|
25
|
+
const q = new URLSearchParams();
|
|
26
|
+
// preserva params existentes (ex.: step) antes de anexar o ponteiro do carrinho
|
|
27
|
+
for (const [k, v] of Object.entries(sp)) {
|
|
28
|
+
if (typeof v === "string" && k !== "id" && k !== "token") q.set(k, v);
|
|
29
|
+
}
|
|
30
|
+
q.set("id", ref.cartId);
|
|
31
|
+
q.set("token", ref.cartToken);
|
|
32
|
+
const freq = await getRecurFreq();
|
|
33
|
+
if (freq && !q.has("freq")) q.set("freq", freq);
|
|
34
|
+
redirect(`/checkout?${q.toString()}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const [methods, shop, catalog] = await Promise.all([
|
|
38
|
+
mockupOr(getPaymentMethods(), [], "checkout/getPaymentMethods"), // sem métodos não há checkout: falha com credenciais tem que aparecer
|
|
39
|
+
getShopData().catch(() => null),
|
|
40
|
+
getCatalog({ first: 12 }).catch(() => ({ nodes: [] as any[] })),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Ofertas reais para o passo de upsell (produtos do catálogo + quick-add real).
|
|
44
|
+
const upsells: UpsellOffer[] = (catalog.nodes ?? [])
|
|
45
|
+
.map((n: any) => n.product ?? n)
|
|
46
|
+
// só em estoque (não esgotado) e com preço válido
|
|
47
|
+
.filter((rp: any) => rp?.slug && !rp.isSoldOut && rp.variants?.[0]?.pricing?.[0]?.price != null)
|
|
48
|
+
.slice(0, 4)
|
|
49
|
+
.map((rp: any): UpsellOffer => {
|
|
50
|
+
const rprice = resolveProductPrice(rp);
|
|
51
|
+
const rv0 = rp.variants?.[0];
|
|
52
|
+
const price = rv0?.pricing?.[0]?.price ?? 0;
|
|
53
|
+
const compareAt = rv0?.pricing?.[0]?.compareAtPrice?.amount ?? null;
|
|
54
|
+
return {
|
|
55
|
+
productId: rp.productId,
|
|
56
|
+
variantId: rv0?._id ?? "",
|
|
57
|
+
name: rp.title,
|
|
58
|
+
weight: rv0?.title ?? "",
|
|
59
|
+
desc: rp.shortDescription ?? "Combina com o que você já escolheu.",
|
|
60
|
+
imageUrl: rp.imageUrls?.[0] ?? null,
|
|
61
|
+
price,
|
|
62
|
+
displayPrice: rprice.displayPrice,
|
|
63
|
+
oldPrice: compareAt && compareAt > price ? compareAt : null,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<DataLayerReady pageType="checkout" />
|
|
70
|
+
<h1 className="sr-only">Finalizar compra</h1>
|
|
71
|
+
<CheckoutClient
|
|
72
|
+
methods={methods as any}
|
|
73
|
+
maxInstallments={shop?.settings?.maxInstallments ?? 12}
|
|
74
|
+
acceptsCard={shop?.acceptsCreditCard ?? true}
|
|
75
|
+
upsells={upsells}
|
|
76
|
+
shopName={shop?.name ?? "Minha Loja"}
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { useParams } from "next/navigation";
|
|
7
|
+
import QRCode from "qrcode";
|
|
8
|
+
import { Copy, Check, Loader2, CheckCircle2, RefreshCw } from "@/lib/icons";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
11
|
+
|
|
12
|
+
export default function PixPage() {
|
|
13
|
+
const params = useParams<{ ref: string }>();
|
|
14
|
+
const ref = params.ref;
|
|
15
|
+
const [emv, setEmv] = React.useState<string | null>(null);
|
|
16
|
+
const [qrDataUrl, setQrDataUrl] = React.useState<string | null>(null);
|
|
17
|
+
const [copied, setCopied] = React.useState(false);
|
|
18
|
+
const [status, setStatus] = React.useState<{ paid: boolean; statusLabel: string } | null>(null);
|
|
19
|
+
const [expired, setExpired] = React.useState(false);
|
|
20
|
+
|
|
21
|
+
// recupera o copia-e-cola do sessionStorage (setado no checkout)
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
try {
|
|
24
|
+
const v = sessionStorage.getItem(`unbox_pix:${ref}`);
|
|
25
|
+
if (v) {
|
|
26
|
+
setEmv(v);
|
|
27
|
+
QRCode.toDataURL(v, { width: 240, margin: 1 }).then(setQrDataUrl).catch(() => {});
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}, [ref]);
|
|
31
|
+
|
|
32
|
+
// polling de status (doc 11): fallback à confirmação por webhook
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
let stop = false;
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
const tick = async () => {
|
|
37
|
+
if (stop) return;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`/api/order/${ref}`, { cache: "no-store" });
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (res.ok && data.order) {
|
|
42
|
+
setStatus({ paid: data.order.paid, statusLabel: data.order.statusLabel });
|
|
43
|
+
if (data.order.paid) return; // para o polling
|
|
44
|
+
if (["CANCELED", "FAILED"].includes(data.order.status)) {
|
|
45
|
+
setExpired(true);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
if (Date.now() - start > 10 * 60 * 1000) {
|
|
51
|
+
setExpired(true);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!stop) setTimeout(tick, 5000);
|
|
55
|
+
};
|
|
56
|
+
const t = setTimeout(tick, 3000);
|
|
57
|
+
return () => {
|
|
58
|
+
stop = true;
|
|
59
|
+
clearTimeout(t);
|
|
60
|
+
};
|
|
61
|
+
}, [ref]);
|
|
62
|
+
|
|
63
|
+
const copy = async () => {
|
|
64
|
+
if (!emv) return;
|
|
65
|
+
await navigator.clipboard.writeText(emv);
|
|
66
|
+
setCopied(true);
|
|
67
|
+
setTimeout(() => setCopied(false), 2000);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (status?.paid) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="mx-auto max-w-md py-10 text-center">
|
|
73
|
+
<CheckCircle2 className="mx-auto size-14 text-primary" />
|
|
74
|
+
<h1 className="mt-4 text-2xl font-bold">Pagamento confirmado!</h1>
|
|
75
|
+
<p className="mt-2 text-muted-foreground">Recebemos seu Pix. Já estamos preparando seu pedido.</p>
|
|
76
|
+
<div className="mt-6 flex justify-center gap-2">
|
|
77
|
+
<Button nativeButton={false} render={<Link href={`/pedido/${ref}`} />}>Ver pedido</Button>
|
|
78
|
+
<Button variant="outline" nativeButton={false} render={<Link href="/produtos" />}>Continuar comprando</Button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="mx-auto max-w-md py-6">
|
|
86
|
+
<h1 className="text-2xl font-bold">Pague com Pix</h1>
|
|
87
|
+
<p className="mt-1 text-sm text-muted-foreground">Pedido #{ref}</p>
|
|
88
|
+
|
|
89
|
+
{expired ? (
|
|
90
|
+
<Alert variant="destructive" className="mt-6">
|
|
91
|
+
<AlertTitle>Pix expirado</AlertTitle>
|
|
92
|
+
<AlertDescription>
|
|
93
|
+
O tempo para pagamento terminou. Refaça o pedido para gerar um novo Pix.
|
|
94
|
+
</AlertDescription>
|
|
95
|
+
</Alert>
|
|
96
|
+
) : (
|
|
97
|
+
<div className="mt-6 rounded-xl border p-6 text-center">
|
|
98
|
+
{qrDataUrl ? (
|
|
99
|
+
<Image src={qrDataUrl} alt="QR Code Pix" width={240} height={240} className="mx-auto rounded-lg" unoptimized />
|
|
100
|
+
) : (
|
|
101
|
+
<div className="mx-auto flex h-[240px] w-[240px] items-center justify-center rounded-lg bg-muted text-sm text-muted-foreground">
|
|
102
|
+
{emv ? "gerando QR..." : "Abra o app do banco e use o código abaixo"}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{emv && (
|
|
107
|
+
<div className="mt-4">
|
|
108
|
+
<p className="mb-1 text-xs text-muted-foreground">Pix copia e cola</p>
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<code className="flex-1 truncate rounded-md bg-muted px-2 py-2 text-left text-xs">{emv}</code>
|
|
111
|
+
<Button type="button" variant="outline" size="icon" onClick={copy} aria-label="Copiar código Pix">
|
|
112
|
+
{copied ? <Check /> : <Copy />}
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<p className="mt-5 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
119
|
+
<Loader2 className="size-4 animate-spin" /> Aguardando confirmação do pagamento…
|
|
120
|
+
</p>
|
|
121
|
+
{status && <p className="mt-1 text-xs text-muted-foreground">Status atual: {status.statusLabel}</p>}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
<div className="mt-4 flex justify-center gap-2">
|
|
126
|
+
<Button variant="outline" nativeButton={false} render={<Link href="/checkout" />}>
|
|
127
|
+
<RefreshCw /> Gerar novo Pix
|
|
128
|
+
</Button>
|
|
129
|
+
<Button variant="ghost" nativeButton={false} render={<Link href={`/pedido/${ref}`} />}>Ver pedido</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { ArrowLeft } from "@phosphor-icons/react/dist/ssr";
|
|
5
|
+
import { getCustomerClient } from "@/lib/customer-session";
|
|
6
|
+
import { getShopData } from "@/lib/queries";
|
|
7
|
+
import { subscriptionStatusLabel } from "@/lib/unbox/customer";
|
|
8
|
+
import { AccountShell } from "@/components/account/account-shell";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { StatusBadge } from "@/components/order-status";
|
|
11
|
+
import { SubscriptionActions } from "@/components/account/subscription-actions";
|
|
12
|
+
|
|
13
|
+
export const metadata: Metadata = { title: "Assinatura", robots: { index: false } };
|
|
14
|
+
|
|
15
|
+
export default async function AssinaturaDetalhePage({ params }: { params: Promise<{ referenceId: string }> }) {
|
|
16
|
+
const me = await getCustomerClient();
|
|
17
|
+
if (!me) redirect("/conta/entrar");
|
|
18
|
+
const { referenceId } = await params;
|
|
19
|
+
|
|
20
|
+
const [sub, shop] = await Promise.all([
|
|
21
|
+
me.subscription(referenceId).catch(() => null),
|
|
22
|
+
getShopData().catch(() => null),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
if (!sub) {
|
|
26
|
+
return (
|
|
27
|
+
<AccountShell title="Assinatura">
|
|
28
|
+
<EmptyState title="Assinatura não encontrada" />
|
|
29
|
+
</AccountShell>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cycles = await me.subscriptionCycles(sub._id, 12).catch(() => ({ nodes: [] }));
|
|
34
|
+
const actions = shop?.recurringOrdersPolicy?.customerActions ?? {};
|
|
35
|
+
const status = sub.status?.value ?? "ACTIVE";
|
|
36
|
+
const card = sub.unboxPayCustomerCreditCard;
|
|
37
|
+
const addr = sub.shippingAddressBook;
|
|
38
|
+
const tone = status === "CANCELED" ? "bad" : status === "PAUSED" ? "warn" : "ok";
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AccountShell title={`Assinatura #${sub.referenceId}`}>
|
|
42
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-6">
|
|
43
|
+
<div className="flex items-center justify-between gap-3">
|
|
44
|
+
<div>
|
|
45
|
+
<p className="text-[12.5px] font-semibold text-[var(--store-muted)]">Frequência</p>
|
|
46
|
+
<p className="font-display text-[18px] font-extrabold text-[var(--store-ink)]">{sub.frequency?.title ?? "—"}</p>
|
|
47
|
+
</div>
|
|
48
|
+
<StatusBadge label={subscriptionStatusLabel(status)} tone={tone} />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="my-4 border-t border-[var(--store-surface-2)]" />
|
|
52
|
+
<dl className="grid gap-3 text-sm sm:grid-cols-2">
|
|
53
|
+
{sub.totalAmount?.displayAmount && <Info label="Valor por ciclo" value={sub.totalAmount.displayAmount} />}
|
|
54
|
+
{sub.cyclesInformation?.nextCycleDate && (
|
|
55
|
+
<Info label="Próxima cobrança" value={new Date(sub.cyclesInformation.nextCycleDate).toLocaleDateString("pt-BR")} />
|
|
56
|
+
)}
|
|
57
|
+
{card && <Info label="Cartão" value={`•••• ${card.last4Digits} (${card.expirationMonth}/${card.expirationYear})`} />}
|
|
58
|
+
{addr && <Info label="Entrega" value={`${addr.address1}, ${addr.number ?? ""} · ${addr.city}/${addr.region}`} />}
|
|
59
|
+
{sub.pricingPolicy?.type === "PERCENTAGE_OFF" && <Info label="Desconto" value={`${sub.pricingPolicy.value}%`} />}
|
|
60
|
+
</dl>
|
|
61
|
+
|
|
62
|
+
{sub.items?.length > 0 && (
|
|
63
|
+
<>
|
|
64
|
+
<div className="my-4 border-t border-[var(--store-surface-2)]" />
|
|
65
|
+
<p className="mb-2 text-[13px] font-bold text-[var(--store-ink-2)]">Itens</p>
|
|
66
|
+
<ul className="flex flex-col gap-1 text-sm text-[var(--store-muted)]">
|
|
67
|
+
{sub.items.map((it: any, i: number) => (
|
|
68
|
+
<li key={i}><span className="font-bold text-[var(--store-primary,#18181B)]">{it.quantity}×</span> {it.productERPCode ?? it.productId}</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<SubscriptionActions
|
|
76
|
+
recurringOrderId={sub._id}
|
|
77
|
+
status={status}
|
|
78
|
+
actions={actions}
|
|
79
|
+
items={(sub.items ?? []).map((it: any) => ({ productId: it.productId, variantId: it.variantId, quantity: it.quantity }))}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
{cycles.nodes?.length > 0 && (
|
|
83
|
+
<div className="mt-6">
|
|
84
|
+
<h2 className="font-display mb-2.5 text-[16px] font-bold text-[var(--store-ink)]">Histórico de ciclos</h2>
|
|
85
|
+
<ul className="flex flex-col divide-y divide-[var(--store-surface-2)] rounded-lg border border-[var(--store-line)] bg-white text-sm">
|
|
86
|
+
{cycles.nodes.map((c: any) => (
|
|
87
|
+
<li key={c._id} className="flex items-center justify-between p-3.5">
|
|
88
|
+
<span className="font-semibold text-[var(--store-ink-2)]">Ciclo #{c.cycleIndex}</span>
|
|
89
|
+
<span className="text-[var(--store-muted)]">
|
|
90
|
+
{c.skipped ? "Pulado" : c.completedAt ? `Concluído em ${new Date(c.completedAt).toLocaleDateString("pt-BR")}` : "Pendente"}
|
|
91
|
+
</span>
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
<Link href="/conta/assinaturas" className="mt-6 inline-flex h-11 items-center gap-2 rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white px-4 text-[14px] font-bold text-[var(--store-ink-2)] no-underline transition-colors hover:border-[var(--store-primary,#18181B)] hover:text-[var(--store-primary,#18181B)]">
|
|
99
|
+
<ArrowLeft weight="bold" /> Voltar
|
|
100
|
+
</Link>
|
|
101
|
+
</AccountShell>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Info({ label, value }: { label: string; value: string }) {
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<dt className="text-[var(--store-muted)]">{label}</dt>
|
|
109
|
+
<dd className="font-semibold text-[var(--store-ink)]">{value}</dd>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { CaretRight, ArrowsClockwise } from "@phosphor-icons/react/dist/ssr";
|
|
5
|
+
import { getCustomerClient } from "@/lib/customer-session";
|
|
6
|
+
import { AccountShell } from "@/components/account/account-shell";
|
|
7
|
+
import { EmptyState } from "@/components/empty-state";
|
|
8
|
+
|
|
9
|
+
export const metadata: Metadata = { title: "Minhas assinaturas", robots: { index: false } };
|
|
10
|
+
|
|
11
|
+
export default async function AssinaturasPage() {
|
|
12
|
+
const me = await getCustomerClient();
|
|
13
|
+
if (!me) redirect("/conta/entrar");
|
|
14
|
+
const subs = await me.subscriptions({ first: 20 }).catch(() => ({ nodes: [], totalCount: 0 }));
|
|
15
|
+
const nodes: any[] = subs.nodes ?? [];
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<AccountShell title="Minhas assinaturas">
|
|
19
|
+
{nodes.length === 0 ? (
|
|
20
|
+
<EmptyState title="Você não tem assinaturas" description="Assine um produto e receba periodicamente com desconto.">
|
|
21
|
+
<Link href="/produtos" className="font-display inline-flex h-11 items-center rounded-xl bg-[var(--store-primary,#18181B)] px-5 text-[14px] font-bold text-white no-underline transition-colors hover:bg-[var(--store-primary-dark,#09090B)]">
|
|
22
|
+
Ver produtos
|
|
23
|
+
</Link>
|
|
24
|
+
</EmptyState>
|
|
25
|
+
) : (
|
|
26
|
+
<div className="flex flex-col gap-3">
|
|
27
|
+
{nodes.map((s) => (
|
|
28
|
+
<Link
|
|
29
|
+
key={s._id}
|
|
30
|
+
href={`/conta/assinaturas/${s.referenceId}`}
|
|
31
|
+
className="group flex items-center justify-between gap-3 rounded-lg border border-[var(--store-line)] bg-white p-4 no-underline transition-all hover:border-[var(--store-primary,#18181B)] hover:shadow-[0_8px_22px_rgba(24,24,27,.08)]"
|
|
32
|
+
>
|
|
33
|
+
<div className="flex items-center gap-3">
|
|
34
|
+
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--store-primary-soft,#F1F1F3)] text-[var(--store-primary,#18181B)]">
|
|
35
|
+
<ArrowsClockwise weight="bold" className="text-[18px]" />
|
|
36
|
+
</span>
|
|
37
|
+
<div>
|
|
38
|
+
<p className="font-display text-[15.5px] font-bold text-[var(--store-ink)]">Assinatura #{s.referenceId}</p>
|
|
39
|
+
<p className="mt-0.5 text-[12.5px] text-[var(--store-muted)]">
|
|
40
|
+
{s.createdAt ? `desde ${new Date(s.createdAt).toLocaleDateString("pt-BR")}` : ""}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<CaretRight weight="bold" className="text-[var(--store-faint)] transition-transform group-hover:translate-x-0.5 group-hover:text-[var(--store-primary,#18181B)]" />
|
|
45
|
+
</Link>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</AccountShell>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { getCustomerClient } from "@/lib/customer-session";
|
|
4
|
+
import { AccountShell } from "@/components/account/account-shell";
|
|
5
|
+
import { AddressBook } from "@/components/account/address-book";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = { title: "Meus endereços", robots: { index: false } };
|
|
8
|
+
|
|
9
|
+
export default async function EnderecosPage() {
|
|
10
|
+
const me = await getCustomerClient();
|
|
11
|
+
if (!me) redirect("/conta/entrar");
|
|
12
|
+
const account = await me.me().catch(() => null);
|
|
13
|
+
const addresses = account?.addressBooks ?? [];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<AccountShell title="Meus endereços">
|
|
17
|
+
<AddressBook initial={addresses} />
|
|
18
|
+
</AccountShell>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
EnvelopeSimple, Key, ArrowLeft, Warning, CircleNotch, ShieldCheck,
|
|
6
|
+
} from "@phosphor-icons/react/dist/ssr";
|
|
7
|
+
|
|
8
|
+
export default function EntrarPage() {
|
|
9
|
+
const [phase, setPhase] = React.useState<"email" | "otp">("email");
|
|
10
|
+
const [email, setEmail] = React.useState("");
|
|
11
|
+
const [otp, setOtp] = React.useState("");
|
|
12
|
+
const [loading, setLoading] = React.useState(false);
|
|
13
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
14
|
+
const [info, setInfo] = React.useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
async function call(url: string, body: any) {
|
|
17
|
+
const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
18
|
+
const data = await res.json().catch(() => ({}));
|
|
19
|
+
if (!res.ok) throw new Error(data?.error || "Erro inesperado.");
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function requestOtp(e: React.FormEvent) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setError(null);
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
await call("/api/account/otp", { email: email.trim() });
|
|
29
|
+
setInfo("Enviamos um código de 6 dígitos para o seu e-mail.");
|
|
30
|
+
setPhase("otp");
|
|
31
|
+
} catch (e: any) {
|
|
32
|
+
setError(e.message);
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function verify(e: React.FormEvent) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
setError(null);
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
await call("/api/account/signin", { email: email.trim(), otp: otp.trim() });
|
|
44
|
+
// Navegação de DOCUMENTO inteiro, não router.push(): o App Router pode reusar o payload
|
|
45
|
+
// RSC de /conta cacheado ANTES do cookie de sessão existir (que era um redirect de volta
|
|
46
|
+
// pra cá) — sintoma "digitei a senha e não entrou; recarreguei e foi" (caso real em produção).
|
|
47
|
+
window.location.assign("/conta");
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
setError(e.message);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="store-layout full-bleed flex min-h-[78vh] items-center justify-center bg-white px-4 py-12 text-[var(--store-ink)]">
|
|
57
|
+
<div className="w-full max-w-[440px]">
|
|
58
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-7 shadow-[0_12px_40px_rgba(24,24,27,.08)] sm:p-9">
|
|
59
|
+
<div className="text-xs font-extrabold tracking-[1.5px] text-[var(--store-primary,#18181B)]">MINHA CONTA</div>
|
|
60
|
+
<h1 className="font-display mt-2 text-[28px] font-extrabold leading-[1.12]">Entrar</h1>
|
|
61
|
+
<p className="mt-1.5 text-[14.5px] leading-[1.5] text-[var(--store-muted)]">
|
|
62
|
+
Acesse com seu e-mail: sem senha, por código de verificação.
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
{error && (
|
|
66
|
+
<div className="mt-5 flex items-start gap-2.5 rounded-xl border border-[var(--store-sale-soft)] bg-[var(--store-sale-soft)] px-4 py-3 text-[13.5px] font-semibold text-[var(--store-sale)]" role="alert">
|
|
67
|
+
<Warning weight="fill" className="mt-px shrink-0 text-[17px] text-[var(--store-sale)]" />
|
|
68
|
+
<span>{error}</span>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{info && phase === "otp" && (
|
|
72
|
+
<div className="mt-5 flex items-start gap-2.5 rounded-xl border border-[var(--store-primary-soft)] bg-[var(--store-primary-soft,#F1F1F3)] px-4 py-3 text-[13.5px] font-semibold text-[var(--store-primary,#18181B)]">
|
|
73
|
+
<ShieldCheck weight="fill" className="mt-px shrink-0 text-[17px]" />
|
|
74
|
+
<span>{info}</span>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{phase === "email" ? (
|
|
79
|
+
<form onSubmit={requestOtp} className="mt-6 flex flex-col gap-4">
|
|
80
|
+
<label className="flex flex-col gap-1.5">
|
|
81
|
+
<span className="text-[13px] font-bold text-[var(--store-ink-2)]">E-mail</span>
|
|
82
|
+
<input
|
|
83
|
+
type="email" autoComplete="email" required value={email}
|
|
84
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
85
|
+
placeholder="voce@email.com"
|
|
86
|
+
className="h-12 w-full rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white px-4 text-[15px] font-medium text-[var(--store-ink)] outline-none transition-colors focus:border-[var(--store-primary,#18181B)]"
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
<button type="submit" disabled={loading || !email.trim()} className="font-display mt-1 flex h-[52px] w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-[var(--store-primary,#18181B)] text-[15px] font-bold tracking-[0.3px] text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
|
|
90
|
+
{loading ? <CircleNotch className="animate-spin text-lg" /> : <EnvelopeSimple weight="bold" className="text-lg" />}
|
|
91
|
+
Enviar código
|
|
92
|
+
</button>
|
|
93
|
+
</form>
|
|
94
|
+
) : (
|
|
95
|
+
<form onSubmit={verify} className="mt-6 flex flex-col gap-4">
|
|
96
|
+
<label className="flex flex-col gap-1.5">
|
|
97
|
+
<span className="text-[13px] font-bold text-[var(--store-ink-2)]">Código recebido</span>
|
|
98
|
+
<input
|
|
99
|
+
inputMode="numeric" autoComplete="one-time-code" maxLength={6} required
|
|
100
|
+
value={otp} onChange={(e) => setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
|
101
|
+
placeholder="000000" autoFocus
|
|
102
|
+
className="h-[58px] w-full rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white text-center font-display text-[26px] font-extrabold tracking-[14px] text-[var(--store-ink)] outline-none transition-colors focus:border-[var(--store-primary,#18181B)]"
|
|
103
|
+
/>
|
|
104
|
+
<span className="text-[12px] text-[var(--store-muted)]">Enviado para <b className="text-[var(--store-ink-2)]">{email}</b></span>
|
|
105
|
+
</label>
|
|
106
|
+
<button type="submit" disabled={loading || otp.length < 4} className="font-display mt-1 flex h-[52px] w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-[var(--store-primary,#18181B)] text-[15px] font-bold tracking-[0.3px] text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
|
|
107
|
+
{loading ? <CircleNotch className="animate-spin text-lg" /> : <Key weight="bold" className="text-lg" />}
|
|
108
|
+
Entrar
|
|
109
|
+
</button>
|
|
110
|
+
<button type="button" onClick={() => { setPhase("email"); setOtp(""); setError(null); }} className="flex cursor-pointer items-center justify-center gap-1.5 text-[13.5px] font-bold text-[var(--store-primary,#18181B)]">
|
|
111
|
+
<ArrowLeft weight="bold" /> Usar outro e-mail
|
|
112
|
+
</button>
|
|
113
|
+
</form>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { Package, Gear, CaretRight } from "@phosphor-icons/react/dist/ssr";
|
|
5
|
+
import { getCustomerClient } from "@/lib/customer-session";
|
|
6
|
+
import { AccountShell } from "@/components/account/account-shell";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = { title: "Minha conta", robots: { index: false } };
|
|
9
|
+
|
|
10
|
+
// Cards enxutos (Assinaturas e Endereços saíram da navegação da conta — ver account-shell.tsx)
|
|
11
|
+
const CARDS = [
|
|
12
|
+
{ href: "/conta/pedidos", label: "Meus pedidos", desc: "Acompanhe status e rastreio", icon: Package },
|
|
13
|
+
{ href: "/conta/preferencias", label: "Preferências", desc: "Notificações e dados da conta", icon: Gear },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default async function ContaPage() {
|
|
17
|
+
const me = await getCustomerClient();
|
|
18
|
+
if (!me) redirect("/conta/entrar");
|
|
19
|
+
|
|
20
|
+
const account = await me.me().catch(() => null);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<AccountShell title="Minha conta">
|
|
24
|
+
<p className="mb-6 text-[15px] text-[var(--store-ink-2)]">
|
|
25
|
+
{account?.email ? <>Olá, <span className="font-bold text-[var(--store-ink)]">{account.email}</span>.</> : "Bem-vindo(a)."}
|
|
26
|
+
{account?.isFirstAccess && " Este é o seu primeiro acesso, confira seus dados."}
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
30
|
+
{CARDS.map(({ href, label, desc, icon: Icon }) => (
|
|
31
|
+
<Link key={href} href={href} className="group flex flex-col rounded-xl border border-[var(--store-line)] bg-white p-5 no-underline transition-all hover:-translate-y-0.5 hover:border-[var(--store-primary,#18181B)] hover:shadow-[0_10px_28px_rgba(24,24,27,.10)]">
|
|
32
|
+
<span className="flex h-11 w-11 items-center justify-center rounded-xl bg-[var(--store-primary-soft,#F1F1F3)] text-[var(--store-primary,#18181B)]">
|
|
33
|
+
<Icon weight="bold" className="text-[22px]" />
|
|
34
|
+
</span>
|
|
35
|
+
<div className="mt-3.5 font-display text-[16px] font-bold text-[var(--store-ink)]">{label}</div>
|
|
36
|
+
<div className="mt-0.5 text-[13px] text-[var(--store-muted)]">{desc}</div>
|
|
37
|
+
<span className="mt-3 inline-flex items-center gap-1 text-[13px] font-bold text-[var(--store-primary,#18181B)]">
|
|
38
|
+
Acessar <CaretRight weight="bold" className="text-[11px] transition-transform group-hover:translate-x-0.5" />
|
|
39
|
+
</span>
|
|
40
|
+
</Link>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</AccountShell>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { ArrowLeft } from "@phosphor-icons/react/dist/ssr";
|
|
5
|
+
import { getCustomerClient } from "@/lib/customer-session";
|
|
6
|
+
import { getOwnedOrder } from "@/lib/orders";
|
|
7
|
+
import { AccountShell } from "@/components/account/account-shell";
|
|
8
|
+
import { OrderStatusCard } from "@/components/order-status";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { ReorderButton, type ReorderItem } from "@/components/account/reorder-button";
|
|
11
|
+
import { mockupOr } from "@/lib/mockup";
|
|
12
|
+
|
|
13
|
+
export const metadata: Metadata = { title: "Detalhe do pedido", robots: { index: false } };
|
|
14
|
+
|
|
15
|
+
export default async function PedidoDetalhePage({ params }: { params: Promise<{ referenceId: string }> }) {
|
|
16
|
+
const me = await getCustomerClient();
|
|
17
|
+
if (!me) redirect("/conta/entrar");
|
|
18
|
+
const { referenceId } = await params;
|
|
19
|
+
const order = await mockupOr(getOwnedOrder(referenceId), null, "pedido/getOwnedOrder");
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<AccountShell title={`Pedido #${referenceId}`}>
|
|
23
|
+
{!order ? (
|
|
24
|
+
<EmptyState title="Pedido não encontrado" />
|
|
25
|
+
) : (
|
|
26
|
+
<>
|
|
27
|
+
<OrderStatusCard order={order} />
|
|
28
|
+
<div className="mt-4 flex flex-wrap gap-2.5">
|
|
29
|
+
<ReorderButton
|
|
30
|
+
items={order.items
|
|
31
|
+
.filter((it) => it.productId && it.variantId && it.price != null)
|
|
32
|
+
.map((it): ReorderItem => ({
|
|
33
|
+
productId: it.productId!,
|
|
34
|
+
variantId: it.variantId!,
|
|
35
|
+
price: it.price!,
|
|
36
|
+
quantity: it.quantity,
|
|
37
|
+
title: it.title,
|
|
38
|
+
thumbnail: it.thumbnail,
|
|
39
|
+
}))}
|
|
40
|
+
/>
|
|
41
|
+
<Link href="/conta/pedidos" className="inline-flex h-11 items-center gap-2 rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white px-4 text-[14px] font-bold text-[var(--store-ink-2)] no-underline transition-colors hover:border-[var(--store-primary,#18181B)] hover:text-[var(--store-primary,#18181B)]">
|
|
42
|
+
<ArrowLeft weight="bold" /> Voltar aos pedidos
|
|
43
|
+
</Link>
|
|
44
|
+
</div>
|
|
45
|
+
</>
|
|
46
|
+
)}
|
|
47
|
+
</AccountShell>
|
|
48
|
+
);
|
|
49
|
+
}
|