@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,257 @@
|
|
|
1
|
+
# Agente 17 — AEO (Answer Engine Optimization)
|
|
2
|
+
|
|
3
|
+
## Objetivo
|
|
4
|
+
|
|
5
|
+
Preparar o storefront para ser **citado e recomendado por motores de resposta** (ChatGPT,
|
|
6
|
+
Claude, Perplexity, Google AI Overviews). Complementa o SEO clássico (agentes 11 e 14):
|
|
7
|
+
enquanto SEO otimiza pra ranquear em lista de links, AEO otimiza pra loja SER a resposta —
|
|
8
|
+
dados estruturados completos, conteúdo que responde perguntas diretamente e acesso liberado
|
|
9
|
+
aos crawlers de IA.
|
|
10
|
+
|
|
11
|
+
**Pré-requisitos:** a foundation já traz `app/sitemap.ts`, `app/robots.ts` (com os bots de
|
|
12
|
+
IA liberados) e JSON-LD Product inline em `app/(loja)/produto/[productSlug]/page.tsx`. Se o
|
|
13
|
+
agente 14 (SEO) já rodou, **leia o que ele fez antes de editar** — os módulos 3 e 6 estendem
|
|
14
|
+
os mesmos arquivos. Rode DEPOIS do briefing (15) e do QA visual (16): AEO em cima de
|
|
15
|
+
conteúdo placeholder é otimizar mentira.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Apresentação ao usuário
|
|
20
|
+
|
|
21
|
+
Ao iniciar este agente, exiba o menu abaixo e pergunte quais módulos ativar:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
Unbox AI AEO Agent — Módulos disponíveis
|
|
26
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
27
|
+
|
|
28
|
+
[1] llms.txt
|
|
29
|
+
JÁ EXISTE como rota (app/llms.txt/route.ts), montada do catálogo real:
|
|
30
|
+
produtos com preço, esgotados marcados, categorias e políticas. O que
|
|
31
|
+
falta é a APRESENTAÇÃo da marca (o que ela faz, para quem, diferenciais):
|
|
32
|
+
preencha NEXT_PUBLIC_SITE_DESCRIPTION e estenda lib/llms-txt.ts. Não
|
|
33
|
+
recrie o arquivo em public/: lá ele esconderia a rota e nasceria velho.
|
|
34
|
+
produtos-chave (nome, preço, link) e políticas —
|
|
35
|
+
gerada do catálogo, mesma fonte do sitemap
|
|
36
|
+
⚙️ Sem dependências
|
|
37
|
+
|
|
38
|
+
[2] FAQ answer-friendly + FAQPage schema
|
|
39
|
+
Resposta direta de 40-60 palavras no 1º parágrafo
|
|
40
|
+
de cada pergunta + JSON-LD FAQPage
|
|
41
|
+
⚙️ Requer: FAQ REAL da marca (verbatim — nunca inventar)
|
|
42
|
+
|
|
43
|
+
[3] Product schema enriquecido
|
|
44
|
+
shippingDetails, hasMerchantReturnPolicy, SKU/GTIN
|
|
45
|
+
quando houver — estende o JSON-LD da PDP
|
|
46
|
+
⚙️ Requer: políticas reais de frete/troca da loja
|
|
47
|
+
|
|
48
|
+
[4] FAQ por produto na PDP
|
|
49
|
+
Bloco "Perguntas sobre este produto" + FAQPage por PDP,
|
|
50
|
+
derivado da ficha técnica real do produto
|
|
51
|
+
⚙️ Requer: ficha técnica/atributos reais no catálogo
|
|
52
|
+
|
|
53
|
+
[5] Política de bots de resposta (conferência)
|
|
54
|
+
A foundation JÁ libera GPTBot, ClaudeBot, PerplexityBot
|
|
55
|
+
etc. — conferir/estender a lista em app/robots.ts
|
|
56
|
+
⚙️ Sem dependências (decisão reversível da marca)
|
|
57
|
+
|
|
58
|
+
[6] Entidade da marca
|
|
59
|
+
Organization schema completo (sameAs com todas as redes)
|
|
60
|
+
+ página Sobre estruturada para citação
|
|
61
|
+
⚙️ Requer: links reais das redes (briefing/rodapé)
|
|
62
|
+
|
|
63
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
64
|
+
Opções: "all", números separados por vírgula (ex: 1,5,6), ou "none"
|
|
65
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Aguarde a resposta antes de qualquer implementação.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Módulos — especificação técnica
|
|
73
|
+
|
|
74
|
+
### [1] llms.txt
|
|
75
|
+
|
|
76
|
+
**Arquivo:** `app/llms.txt/route.ts` (novo)
|
|
77
|
+
|
|
78
|
+
Rota que devolve `text/plain` no padrão [llms.txt](https://llmstxt.org): um sumário da loja
|
|
79
|
+
em Markdown, pensado pra um LLM consumir em uma leitura. Gerar dinamicamente da MESMA fonte
|
|
80
|
+
do sitemap (catálogo via `withStoreClient()`; em modo mockup, `lib/enrichment/products.json`
|
|
81
|
+
— não pode quebrar o build sem credenciais):
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// app/llms.txt/route.ts
|
|
85
|
+
export async function GET() {
|
|
86
|
+
const body = [
|
|
87
|
+
`# ${nomeDaLoja}`,
|
|
88
|
+
``,
|
|
89
|
+
`> ${descricaoDaLoja}`, // 1-2 frases: o que a loja vende e para quem (do briefing)
|
|
90
|
+
``,
|
|
91
|
+
`## Categorias`,
|
|
92
|
+
...tags.map((t) => `- [${t.name}](${URL}/categoria/${t.slug})`),
|
|
93
|
+
``,
|
|
94
|
+
`## Produtos principais`,
|
|
95
|
+
...produtos.slice(0, 20).map(
|
|
96
|
+
(p) => `- [${p.title}](${URL}/produto/${p.slug}) — R$ ${preco(p)}`
|
|
97
|
+
),
|
|
98
|
+
``,
|
|
99
|
+
`## Políticas`,
|
|
100
|
+
`- [Trocas e devoluções](${URL}/devolucoes)`,
|
|
101
|
+
`- [Termos](${URL}/termos)`,
|
|
102
|
+
].join("\n");
|
|
103
|
+
return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Confira os caminhos REAIS das páginas de política desta loja antes de linkar (a foundation
|
|
108
|
+
traz `/devolucoes`, `/termos`, `/privacidade`; o briefing pode ter criado outras).
|
|
109
|
+
|
|
110
|
+
⚠️ **A foundation já traz um `public/llms.txt` ESTÁTICO de partida** — e em Next.js o
|
|
111
|
+
arquivo em `public/` vence o route handler: a rota nova nunca responderia. Ao criar a rota
|
|
112
|
+
dinâmica, **apague `public/llms.txt`** no mesmo commit (uma fonte de verdade só). Se
|
|
113
|
+
preferir manter o estático, atualize-o à mão e NÃO crie a rota.
|
|
114
|
+
|
|
115
|
+
### [2] FAQ answer-friendly + FAQPage schema
|
|
116
|
+
|
|
117
|
+
**Arquivos:** onde o FAQ da loja vive — na foundation, o bloco de FAQ da PDP
|
|
118
|
+
(`components/product/pdp/interactive.tsx`, `FaqList`) e/ou a página institucional que o
|
|
119
|
+
briefing tiver criado.
|
|
120
|
+
|
|
121
|
+
Answer engines extraem respostas curtas e autocontidas. Reestruture cada item pra que o
|
|
122
|
+
**primeiro parágrafo responda a pergunta em 40-60 palavras**, sem depender do contexto ao
|
|
123
|
+
redor ("Sim, trocamos em até 7 dias (CDC). Basta..." e não "Conforme mencionado acima...").
|
|
124
|
+
Detalhes vêm depois do parágrafo de resposta.
|
|
125
|
+
|
|
126
|
+
JSON-LD na página (sempre via `JSON.stringify`):
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const faqSchema = {
|
|
130
|
+
"@context": "https://schema.org",
|
|
131
|
+
"@type": "FAQPage",
|
|
132
|
+
mainEntity: faq.map((item) => ({
|
|
133
|
+
"@type": "Question",
|
|
134
|
+
name: item.pergunta,
|
|
135
|
+
acceptedAnswer: { "@type": "Answer", text: item.respostaDireta },
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Regra dura:** o conteúdo vem do FAQ REAL da marca (site atual ou briefing), verbatim no
|
|
141
|
+
sentido — pode reescrever a FORMA pra ficar direto, nunca inventar pergunta ou resposta.
|
|
142
|
+
Sem FAQ real → módulo não roda, vira pendência.
|
|
143
|
+
|
|
144
|
+
### [3] Product schema enriquecido
|
|
145
|
+
|
|
146
|
+
**Arquivo:** `app/(loja)/produto/[productSlug]/page.tsx` (estender o objeto `jsonLd`
|
|
147
|
+
existente — **leia antes**; o QA de honestidade já condicionou o `aggregateRating` a
|
|
148
|
+
avaliações reais, não regrida isso).
|
|
149
|
+
|
|
150
|
+
Answer engines pesam muito frete e política de troca ao recomendar onde comprar. Adicionar
|
|
151
|
+
à `Offer`:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
offers: {
|
|
155
|
+
"@type": "Offer",
|
|
156
|
+
// ...campos existentes (price, availability, url)
|
|
157
|
+
shippingDetails: {
|
|
158
|
+
"@type": "OfferShippingDetails",
|
|
159
|
+
shippingDestination: { "@type": "DefinedRegion", addressCountry: "BR" },
|
|
160
|
+
// deliveryTime só se a loja tiver prazo padrão REAL declarado
|
|
161
|
+
},
|
|
162
|
+
hasMerchantReturnPolicy: {
|
|
163
|
+
"@type": "MerchantReturnPolicy",
|
|
164
|
+
applicableCountry: "BR",
|
|
165
|
+
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
|
|
166
|
+
merchantReturnDays: 7, // CDC: 7 dias de arrependimento; ajuste se a política real for maior
|
|
167
|
+
returnMethod: "https://schema.org/ReturnByMail",
|
|
168
|
+
returnFees: "https://schema.org/FreeReturn", // só se for verdade
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
sku: p.sku ?? undefined,
|
|
172
|
+
gtin13: p.gtin ?? undefined, // só quando o campo existir no catálogo
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Valores de política **têm que refletir a política real** (páginas legais/briefing/decisão
|
|
176
|
+
registrada do lojista). Na dúvida, o mínimo legal (7 dias CDC) e um TODO pro humano
|
|
177
|
+
confirmar.
|
|
178
|
+
|
|
179
|
+
### [4] FAQ por produto na PDP
|
|
180
|
+
|
|
181
|
+
**Arquivos:** `components/product/pdp/product-faq.tsx` (novo) + inserção na PDP + FAQPage
|
|
182
|
+
JSON-LD por produto.
|
|
183
|
+
|
|
184
|
+
Bloco "Perguntas sobre este produto" com 3-5 perguntas derivadas **da ficha técnica real**
|
|
185
|
+
(peso, medidas, uso, cuidados — o que existir em `lib/enrichment/products.json` ou nos
|
|
186
|
+
atributos do catálogo). Exemplo honesto: "Qual o peso do produto?" → resposta com o dado
|
|
187
|
+
real.
|
|
188
|
+
|
|
189
|
+
- JSON-LD FAQPage no nível da PDP com essas perguntas.
|
|
190
|
+
- **Nunca inventar** resposta que o dado não sustenta; produto sem ficha suficiente fica
|
|
191
|
+
sem o bloco (renderiza null).
|
|
192
|
+
|
|
193
|
+
### [5] Política de bots de resposta (conferência)
|
|
194
|
+
|
|
195
|
+
**Arquivo:** `app/robots.ts`
|
|
196
|
+
|
|
197
|
+
A foundation JÁ libera os crawlers de IA (lista `AI_AGENTS`: GPTBot, OAI-SearchBot,
|
|
198
|
+
ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User,
|
|
199
|
+
Google-Extended, Applebot-Extended, Amazonbot, meta-externalagent, CCBot) com os mesmos
|
|
200
|
+
`disallow` de conta/carrinho/checkout/pedido/api. Neste módulo: confira que ninguém removeu
|
|
201
|
+
a lista, acrescente bots novos que surgirem, e registre no commit que a permissão é decisão
|
|
202
|
+
reversível da marca. Regra por userAgent específico SUBSTITUI a genérica — os `disallow`
|
|
203
|
+
precisam estar replicados em cada regra (a foundation já faz).
|
|
204
|
+
|
|
205
|
+
### [6] Entidade da marca
|
|
206
|
+
|
|
207
|
+
**Arquivos:** `app/(loja)/page.tsx` (Organization + WebSite JÁ existem: estenda com sameAs/logo, não duplique) +
|
|
208
|
+
página Sobre.
|
|
209
|
+
|
|
210
|
+
Answer engines montam um "cartão" da marca cruzando fontes. Ajude:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const orgSchema = {
|
|
214
|
+
"@context": "https://schema.org",
|
|
215
|
+
"@type": "Organization",
|
|
216
|
+
name: nomeDaLoja,
|
|
217
|
+
url: siteUrl,
|
|
218
|
+
logo: `${siteUrl}/brand/logo.svg`,
|
|
219
|
+
description: descricaoDaMarca, // 1 frase, do briefing
|
|
220
|
+
sameAs: [instagramUrl /* todas as redes REAIS do rodapé/briefing */],
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Se o agente 14 (módulo 1) já criou um Organization schema, **estenda** o existente com
|
|
225
|
+
`description` e `sameAs` completos — nunca duplique o bloco.
|
|
226
|
+
|
|
227
|
+
Página **Sobre**: garanta que existe (crie em `app/(loja)/sobre/page.tsx` se o briefing
|
|
228
|
+
tiver o conteúdo) e que o primeiro parágrafo define a marca de forma autocontida (quem é, o
|
|
229
|
+
que vende, desde quando, diferencial) — é o parágrafo que um answer engine cita. Conteúdo
|
|
230
|
+
do briefing (Entendimento da Marca / Posicionamento); nada inventado.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Fluxo do agente
|
|
235
|
+
|
|
236
|
+
1. **Apresentar o menu** e aguardar a escolha.
|
|
237
|
+
2. **Verificar pré-requisitos:** `app/robots.ts` e `app/sitemap.ts` existem; ler o que os
|
|
238
|
+
agentes 11/14 já implementaram (e o que o QA de honestidade já condicionou no JSON-LD).
|
|
239
|
+
3. **Implementar em ordem numérica** — módulos independentes entre si.
|
|
240
|
+
4. **Ao final:** listar o que foi implementado, com os TODOs que exigem confirmação humana
|
|
241
|
+
(política de troca real, GTIN, redes sociais faltantes) — pendências no formato do
|
|
242
|
+
briefing (bloqueiam o go-live, não o build).
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Regras
|
|
247
|
+
|
|
248
|
+
- **Você nunca inventa dado** (regra de ouro da loja): FAQ verbatim; políticas de
|
|
249
|
+
frete/troca reais; ficha técnica real. O que não houver, não entra — vira pendência
|
|
250
|
+
listada. Decisão de manter algo sem fonte é do LOJISTA, registrada
|
|
251
|
+
(`marca/honestidade-permitido.txt`).
|
|
252
|
+
- **Nunca duplicar** schema que 11/14 já emitem — leia os arquivos antes de editar.
|
|
253
|
+
- `<script type="application/ld+json">` sempre via `JSON.stringify()` (XSS com interpolação
|
|
254
|
+
direta).
|
|
255
|
+
- `llms.txt` e `robots.ts` devem funcionar em modo mockup (catálogo de exemplo) sem quebrar
|
|
256
|
+
o build — rode `npm run build` ao final.
|
|
257
|
+
- Textos em português do Brasil, no tom de voz do briefing (sem travessão na copy).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { CaretRight } from "@phosphor-icons/react/dist/ssr";
|
|
4
|
+
import { getCatalog, getTopTags } from "@/lib/queries";
|
|
5
|
+
import { PAGE_SIZE } from "@/lib/catalog";
|
|
6
|
+
import { buildTagMap, mapCatalogItems } from "@/lib/catalog-map";
|
|
7
|
+
import { ProductGrid } from "@/components/catalog/product-grid";
|
|
8
|
+
import { Pager } from "@/components/catalog/pager";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { SearchBox } from "@/components/search-box";
|
|
11
|
+
import { mockupOr } from "@/lib/mockup";
|
|
12
|
+
import { DataLayerReady } from "@/components/analytics/data-layer-ready";
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = { title: "Busca", robots: { index: false } };
|
|
15
|
+
|
|
16
|
+
type SP = { q?: string; page?: string };
|
|
17
|
+
|
|
18
|
+
export default async function BuscaPage({ searchParams }: { searchParams: Promise<SP> }) {
|
|
19
|
+
const sp = await searchParams;
|
|
20
|
+
const q = (sp.q ?? "").trim();
|
|
21
|
+
const page = Math.max(1, parseInt(sp.page ?? "1", 10) || 1);
|
|
22
|
+
const offset = (page - 1) * PAGE_SIZE;
|
|
23
|
+
|
|
24
|
+
const [catalog, tags] = await Promise.all([
|
|
25
|
+
q
|
|
26
|
+
? mockupOr(getCatalog({ first: PAGE_SIZE, offset, searchText: q }), ({ nodes: [], totalCount: 0 } as any), "busca/getCatalog")
|
|
27
|
+
: Promise.resolve({ nodes: [], totalCount: 0 } as any),
|
|
28
|
+
mockupOr(getTopTags(), [], "busca/getTopTags"),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const items = mapCatalogItems(catalog.nodes ?? [], buildTagMap(tags as any[]));
|
|
32
|
+
const total = catalog.totalCount ?? 0;
|
|
33
|
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
34
|
+
const hrefForPage = (p: number) => `/busca?q=${encodeURIComponent(q)}${p > 1 ? `&page=${p}` : ""}`;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="store-layout full-bleed bg-white text-[var(--store-ink)]">
|
|
38
|
+
<DataLayerReady pageType="search" />
|
|
39
|
+
<div className="mx-auto max-w-[1240px] px-4 py-7 sm:px-6">
|
|
40
|
+
{/* breadcrumb */}
|
|
41
|
+
<div className="flex items-center gap-2 text-[13px] font-medium text-[var(--store-muted)]">
|
|
42
|
+
<Link href="/" className="no-underline hover:text-[var(--store-ink)]">Início</Link>
|
|
43
|
+
<CaretRight className="text-[11px]" />
|
|
44
|
+
<span className="font-semibold text-[var(--store-ink)]">Busca</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<h1 className="font-display mt-4 text-[28px] font-extrabold leading-[1.12]">Buscar produtos</h1>
|
|
48
|
+
<SearchBox defaultValue={q} className="mt-4 max-w-xl" />
|
|
49
|
+
|
|
50
|
+
<div className="mt-7">
|
|
51
|
+
{!q ? (
|
|
52
|
+
<EmptyState title="Digite o que procura" description="Busque pelo nome do produto ou categoria." />
|
|
53
|
+
) : items.length === 0 ? (
|
|
54
|
+
<EmptyState title={`Nada encontrado para “${q}”`} description="Tente outros termos ou explore o catálogo completo.">
|
|
55
|
+
<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)]">
|
|
56
|
+
Ver todos os produtos
|
|
57
|
+
</Link>
|
|
58
|
+
</EmptyState>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
<p className="mb-4 text-sm text-[var(--store-muted)]">
|
|
62
|
+
<b className="text-[var(--store-ink)]">{total}</b> resultado(s) para “{q}”
|
|
63
|
+
</p>
|
|
64
|
+
<ProductGrid items={items} />
|
|
65
|
+
<Pager page={page} totalPages={totalPages} hrefForPage={hrefForPage} />
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Passo 2 do funil (/carrinho/oferta): o cliente distribui a quantidade escolhida entre os
|
|
2
|
+
// produtos, escolhe a frequência de envio (assinatura REAL da política da loja, se houver)
|
|
3
|
+
// e segue pro checkout. Tela split sem o chrome do site. Produtos e preços do catálogo.
|
|
4
|
+
import type { Metadata } from "next";
|
|
5
|
+
import { Suspense } from "react";
|
|
6
|
+
import { getCatalog, getTopTags, getShopData } from "@/lib/queries";
|
|
7
|
+
import { buildTagMap, mapCatalogItems } from "@/lib/catalog-map";
|
|
8
|
+
import { ProductPicker, type PickerSubscription } from "@/components/landing/product-picker";
|
|
9
|
+
import { mockupOr } from "@/lib/mockup";
|
|
10
|
+
|
|
11
|
+
export const revalidate = 300;
|
|
12
|
+
export const metadata: Metadata = { title: "Monte seu pedido", robots: { index: false } };
|
|
13
|
+
|
|
14
|
+
const isCombo = (s: string) => /kit|combo/i.test(s);
|
|
15
|
+
|
|
16
|
+
export default async function CarrinhoOfertaPage() {
|
|
17
|
+
const [catalog, tags, shop] = await Promise.all([
|
|
18
|
+
mockupOr(getCatalog({ first: 100 }), ({ nodes: [] as any[] }), "carrinho-oferta/getCatalog"),
|
|
19
|
+
mockupOr(getTopTags(), [], "carrinho-oferta/getTopTags"),
|
|
20
|
+
mockupOr(getShopData(), null, "carrinho-oferta/getShopData"),
|
|
21
|
+
]);
|
|
22
|
+
const nodes = (catalog.nodes ?? []) as any[];
|
|
23
|
+
const items = mapCatalogItems(nodes, buildTagMap(tags as any[]));
|
|
24
|
+
|
|
25
|
+
// Mesma seleção da landing: combos → ofertas → destaques (o herói vem primeiro).
|
|
26
|
+
let list = items.filter((it) => it.categories.some(isCombo));
|
|
27
|
+
if (list.length < 2) {
|
|
28
|
+
const deals = items.filter((it) => it.oldPrice != null);
|
|
29
|
+
list = deals.length >= 2 ? deals : items.slice(0, 8);
|
|
30
|
+
}
|
|
31
|
+
list = list.slice(0, 8);
|
|
32
|
+
|
|
33
|
+
// Assinatura: política REAL da loja + produtos que permitem recorrência.
|
|
34
|
+
const recurrableIds = nodes
|
|
35
|
+
.map((n: any) => n.product ?? n)
|
|
36
|
+
.filter((p: any) => p?.recurrenceAllowed)
|
|
37
|
+
.map((p: any) => String(p.productId));
|
|
38
|
+
const policy = (shop as any)?.recurringOrdersPolicy;
|
|
39
|
+
const subscription: PickerSubscription | null =
|
|
40
|
+
policy?.enabled && (policy.allowedFrequencies?.length ?? 0) > 0 && recurrableIds.length > 0
|
|
41
|
+
? {
|
|
42
|
+
percentOff: policy.pricingPolicy?.type === "PERCENTAGE_OFF" ? policy.pricingPolicy.value : null,
|
|
43
|
+
frequencies: (policy.allowedFrequencies ?? []).map((f: any) => ({ id: f._id, title: f.title })),
|
|
44
|
+
}
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Suspense>
|
|
49
|
+
<ProductPicker
|
|
50
|
+
products={list}
|
|
51
|
+
shopName={(shop as any)?.name ?? "Nossa loja"}
|
|
52
|
+
subscription={subscription}
|
|
53
|
+
recurrableIds={recurrableIds}
|
|
54
|
+
/>
|
|
55
|
+
</Suspense>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import { Trash2, Tag, ArrowRight } from "@/lib/icons";
|
|
8
|
+
import { useCart } from "@/components/cart/cart-provider";
|
|
9
|
+
import { QuantityStepper } from "@/components/quantity-stepper";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { Badge } from "@/components/ui/badge";
|
|
13
|
+
import { Separator } from "@/components/ui/separator";
|
|
14
|
+
import { EmptyState } from "@/components/empty-state";
|
|
15
|
+
import { formatBRL } from "@/lib/format";
|
|
16
|
+
import { goToCheckout } from "@/lib/checkout-nav";
|
|
17
|
+
import { trackViewCart, trackPageType } from "@/lib/analytics";
|
|
18
|
+
|
|
19
|
+
export default function CarrinhoPage() {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const { cart, updateQty, remove, applyCoupon, removeCoupon, appliedCoupon, loading } = useCart();
|
|
22
|
+
const [code, setCode] = React.useState("");
|
|
23
|
+
const items = cart?.items ?? [];
|
|
24
|
+
const hasDiscount = !!appliedCoupon || (!!cart?.summary.discountTotal && cart.summary.discountTotal !== "R$0,00");
|
|
25
|
+
|
|
26
|
+
// dataLayerReady(cart) + view_cart — uma vez por carregamento com item dentro.
|
|
27
|
+
const trackedRef = React.useRef(false);
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (trackedRef.current || !cart?.items?.length) return;
|
|
30
|
+
trackedRef.current = true;
|
|
31
|
+
const list = cart.items.filter((i) => !i.isBonus).map((i) => ({ id: i.productId, name: i.title, variant: i.variantTitle, price: i.unitPrice, quantity: i.quantity }));
|
|
32
|
+
trackPageType("cart", list);
|
|
33
|
+
trackViewCart(list, cart.summary.totalAmount);
|
|
34
|
+
}, [cart]);
|
|
35
|
+
|
|
36
|
+
if (!cart || items.length === 0) {
|
|
37
|
+
return (
|
|
38
|
+
<EmptyState title="Seu carrinho está vazio" description="Que tal explorar nossos produtos?">
|
|
39
|
+
<Button nativeButton={false} render={<Link href="/produtos" />}>Ver produtos</Button>
|
|
40
|
+
</EmptyState>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<h1 className="mb-6 text-2xl font-bold">Seu carrinho</h1>
|
|
47
|
+
<div className="grid gap-8 lg:grid-cols-[1fr_340px]">
|
|
48
|
+
{/* Itens */}
|
|
49
|
+
<ul className="divide-y rounded-xl border">
|
|
50
|
+
{items.map((it) => (
|
|
51
|
+
<li key={it.id} className="flex gap-4 p-4">
|
|
52
|
+
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-md bg-muted">
|
|
53
|
+
{it.thumbnail ? (
|
|
54
|
+
<Image src={it.thumbnail} alt={it.title} fill sizes="80px" className="object-cover" />
|
|
55
|
+
) : (
|
|
56
|
+
<div className="flex h-full items-center justify-center text-[10px] text-muted-foreground">sem foto</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex flex-1 flex-col">
|
|
60
|
+
<div className="flex items-start justify-between gap-3">
|
|
61
|
+
<div>
|
|
62
|
+
<p className="font-medium">{it.title}</p>
|
|
63
|
+
{it.variantTitle && <p className="text-sm text-muted-foreground">{it.variantTitle}</p>}
|
|
64
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
65
|
+
{it.isRecurring && (
|
|
66
|
+
<Badge variant="outline">
|
|
67
|
+
Assinatura{cart.recurringFrequencyTitle ? ` · ${cart.recurringFrequencyTitle}` : ""}
|
|
68
|
+
</Badge>
|
|
69
|
+
)}
|
|
70
|
+
{it.isBonus && <Badge>🎁 Brinde</Badge>}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="text-right">
|
|
74
|
+
<p className="font-medium tabular-nums">{it.isBonus ? "Grátis" : it.displayUnitPrice}</p>
|
|
75
|
+
{!it.isBonus && <p className="text-xs text-muted-foreground">cada</p>}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{!it.isBonus && (
|
|
79
|
+
<div className="mt-3 flex items-center justify-between gap-3">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<QuantityStepper value={it.quantity} onChange={(q) => updateQty(it.id, q)} disabled={loading} />
|
|
82
|
+
<Button variant="ghost" size="sm" onClick={() => remove(it.id)} disabled={loading}>
|
|
83
|
+
<Trash2 /> Remover
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
{it.quantity > 1 && (
|
|
87
|
+
<span className="text-sm font-medium tabular-nums">{formatBRL(it.unitPrice * it.quantity)}</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
|
|
96
|
+
{/* Resumo */}
|
|
97
|
+
<aside className="h-fit rounded-xl border p-5">
|
|
98
|
+
<h2 className="mb-3 font-semibold">Resumo</h2>
|
|
99
|
+
|
|
100
|
+
{/* Cupom */}
|
|
101
|
+
<form
|
|
102
|
+
className="mb-4"
|
|
103
|
+
onSubmit={async (e) => {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
if (code.trim() && (await applyCoupon(code.trim()))) setCode("");
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{hasDiscount ? (
|
|
109
|
+
<div className="flex items-center justify-between rounded-md bg-muted px-3 py-2 text-sm">
|
|
110
|
+
<span className="flex items-center gap-1.5">
|
|
111
|
+
<Tag className="size-3.5" /> {appliedCoupon?.code ?? "Desconto aplicado"}
|
|
112
|
+
</span>
|
|
113
|
+
{appliedCoupon?.discountId && (
|
|
114
|
+
<button type="button" className="text-muted-foreground hover:text-destructive" onClick={() => removeCoupon()}>
|
|
115
|
+
remover
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="flex gap-2">
|
|
121
|
+
<Input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Cupom de desconto" aria-label="Cupom" />
|
|
122
|
+
<Button type="submit" variant="outline" disabled={loading || !code.trim()}>Aplicar</Button>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</form>
|
|
126
|
+
|
|
127
|
+
<Separator className="my-3" />
|
|
128
|
+
<dl className="space-y-1.5 text-sm">
|
|
129
|
+
<Row label="Subtotal" value={cart.summary.itemTotal} />
|
|
130
|
+
{cart.summary.discountTotal && cart.summary.discountTotal !== "R$0,00" && (
|
|
131
|
+
<Row label="Desconto" value={`- ${cart.summary.discountTotal}`} accent />
|
|
132
|
+
)}
|
|
133
|
+
<Row label="Frete" value={cart.summary.shippingTotal ?? "calculado no checkout"} />
|
|
134
|
+
</dl>
|
|
135
|
+
<Separator className="my-3" />
|
|
136
|
+
<div className="flex items-center justify-between text-base font-semibold">
|
|
137
|
+
<span>Total</span>
|
|
138
|
+
<span>{cart.summary.total ?? "—"}</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<Button className="mt-4 w-full" size="lg" onClick={() => goToCheckout(router)}>
|
|
142
|
+
Finalizar compra <ArrowRight />
|
|
143
|
+
</Button>
|
|
144
|
+
<Button variant="ghost" className="mt-2 w-full" nativeButton={false} render={<Link href="/produtos" />}>
|
|
145
|
+
Continuar comprando
|
|
146
|
+
</Button>
|
|
147
|
+
</aside>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Row({ label, value, accent }: { label: string; value?: string; accent?: boolean }) {
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex justify-between">
|
|
156
|
+
<dt className="text-muted-foreground">{label}</dt>
|
|
157
|
+
<dd className={`tabular-nums ${accent ? "font-medium text-[var(--store-primary)]" : ""}`}>{value ?? "—"}</dd>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ═══ POR QUE ESTE LAYOUT EXISTE (não é organização de código) ═══
|
|
2
|
+
// Ele valida a existência da categoria e chama notFound() ANTES da página. Sem isso a
|
|
3
|
+
// rota devolvia HTTP 200 com a tela de 404 dentro — um soft-404, que faz o Google
|
|
4
|
+
// indexar página vazia, valida rota quebrada em qualquer checagem por status e
|
|
5
|
+
// contamina destino de 301 e canonical.
|
|
6
|
+
//
|
|
7
|
+
// A causa não é óbvia: o notFound() da page.tsx SEMPRE esteve lá e não adiantava. O
|
|
8
|
+
// loading.tsx deste segmento envolve a página num <Suspense>; o shell fica pronto na
|
|
9
|
+
// hora e é enviado com 200, e quando o notFound() acontece o status já foi. O layout
|
|
10
|
+
// renderiza ACIMA dessa fronteira, então aqui o 404 ainda pode ser emitido.
|
|
11
|
+
// Medido: mesma página, só trocando a presença do loading.tsx → 404 vira 200.
|
|
12
|
+
//
|
|
13
|
+
// getTopTags é React cache(): chamar aqui e na página não duplica requisição.
|
|
14
|
+
import { notFound } from "next/navigation";
|
|
15
|
+
import { getTopTags } from "@/lib/queries";
|
|
16
|
+
import { mockupOr } from "@/lib/mockup";
|
|
17
|
+
|
|
18
|
+
export default async function CategoriaLayout({
|
|
19
|
+
children,
|
|
20
|
+
params,
|
|
21
|
+
}: {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
params: Promise<{ tagSlug: string }>;
|
|
24
|
+
}) {
|
|
25
|
+
const { tagSlug } = await params;
|
|
26
|
+
// mockupOr: falha da API com credenciais RELANÇA — nunca vira 404 cacheado.
|
|
27
|
+
const tags = await mockupOr(getTopTags(), [], `categoria/${tagSlug}`);
|
|
28
|
+
const alvo = decodeURIComponent(tagSlug);
|
|
29
|
+
if (!(tags as { slug?: string }[]).some((t) => t.slug === alvo)) notFound();
|
|
30
|
+
return <>{children}</>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CatalogGridSkeleton } from "@/components/catalog/grid-skeleton";
|
|
2
|
+
|
|
3
|
+
export default function Loading() {
|
|
4
|
+
return (
|
|
5
|
+
<div>
|
|
6
|
+
<div className="mb-3 h-5 w-56 animate-pulse rounded bg-muted" />
|
|
7
|
+
<div className="mb-5 flex flex-wrap items-center justify-between gap-3">
|
|
8
|
+
<div className="space-y-2">
|
|
9
|
+
<div className="h-7 w-48 animate-pulse rounded bg-muted" />
|
|
10
|
+
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
|
11
|
+
</div>
|
|
12
|
+
<div className="h-9 w-40 animate-pulse rounded-md bg-muted" />
|
|
13
|
+
</div>
|
|
14
|
+
<CatalogGridSkeleton count={8} />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
import { getCatalog, getTopTags } from "@/lib/queries";
|
|
4
|
+
import { buildTagMap, buildCategories, mapCatalogItems } from "@/lib/catalog-map";
|
|
5
|
+
import { CatalogClient } from "@/components/catalog/catalog-client";
|
|
6
|
+
import { mockupOr } from "@/lib/mockup";
|
|
7
|
+
import { DataLayerReady } from "@/components/analytics/data-layer-ready";
|
|
8
|
+
|
|
9
|
+
export const revalidate = 300;
|
|
10
|
+
|
|
11
|
+
async function findTag(slug: string, tags: any[]) {
|
|
12
|
+
const decoded = decodeURIComponent(slug);
|
|
13
|
+
return (tags as any[]).find((t) => t.slug === decoded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateMetadata({ params }: { params: Promise<{ tagSlug: string }> }): Promise<Metadata> {
|
|
17
|
+
const { tagSlug } = await params;
|
|
18
|
+
const tags = await mockupOr(getTopTags(), [], "categoria/getTopTags");
|
|
19
|
+
const tag = await findTag(tagSlug, tags as any[]);
|
|
20
|
+
if (!tag) return { title: "Categoria" };
|
|
21
|
+
return { title: tag.displayTitle || tag.name, description: tag.description ?? undefined, alternates: { canonical: `/categoria/${encodeURIComponent(tag.slug)}` } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default async function CategoryPage({ params }: { params: Promise<{ tagSlug: string }> }) {
|
|
25
|
+
const { tagSlug } = await params;
|
|
26
|
+
const [catalog, tags] = await Promise.all([
|
|
27
|
+
mockupOr(getCatalog({ first: 100 }), ({ nodes: [] as any[] }), "categoria/getCatalog"),
|
|
28
|
+
mockupOr(getTopTags(), [], "categoria/getTopTags"),
|
|
29
|
+
]);
|
|
30
|
+
const tag = await findTag(tagSlug, tags as any[]);
|
|
31
|
+
if (!tag) notFound();
|
|
32
|
+
|
|
33
|
+
const tagMap = buildTagMap(tags as any[]);
|
|
34
|
+
const itens = mapCatalogItems(catalog.nodes ?? [], tagMap);
|
|
35
|
+
// EDITOR: a categoria REAPROVEITA o container "catalogo" de /produtos (mesma faixa de confiança, mesmo
|
|
36
|
+
// cabeçalho de kits, mesmo aviso de lista vazia) sem mandar na ordem dele: `layout={false}` faz o
|
|
37
|
+
// manifesto desta página sair com `semLayout`, e o painel explica que a ordem se edita em /produtos.
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<DataLayerReady pageType="category" products={itens.slice(0, 12).map((i) => ({ id: i.productId, name: i.title, price: i.price }))} />
|
|
41
|
+
<CatalogClient
|
|
42
|
+
items={itens}
|
|
43
|
+
categories={buildCategories(tags as any[])}
|
|
44
|
+
initialCategory={tag.displayTitle || tag.name}
|
|
45
|
+
layout={false}
|
|
46
|
+
/>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|