@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,227 @@
|
|
|
1
|
+
// A PÁGINA DE PRODUTO É UM MOLDE.
|
|
2
|
+
//
|
|
3
|
+
// Um valor para TODOS os produtos: o documento do lojista não tem dimensão de produto, de propósito.
|
|
4
|
+
// Nome, preço, fotos, descrição, variantes, tamanhos, estoque, selos regulados e avaliações vêm do
|
|
5
|
+
// catálogo da Unbox e do enriquecimento e NÃO viram primitivo: são DADO, e o gate os desconta por
|
|
6
|
+
// `data-editor-ignore` (README do editor, §8). O que vira primitivo é o que o lojista mudaria SEM mudar
|
|
7
|
+
// o produto: chapéu, links do caminho, selos de confiança, promessa de entrega, título e atributos de
|
|
8
|
+
// qualidade, título e perguntas MODELO do FAQ, chapéu/título/link do catálogo, newsletter.
|
|
9
|
+
//
|
|
10
|
+
// Container PRÓPRIO (`produto`, declarado em lib/rotas-editaveis.ts): a faixa de confiança e a newsletter
|
|
11
|
+
// daqui NÃO reaproveitam o `home`, porque "frete grátis acima de R$ 150" na home e na PDP são frases que
|
|
12
|
+
// o lojista pode querer diferentes (a ordem é do container; a copy é do container). Ids de seção
|
|
13
|
+
// descrevem o PAPEL do bloco e são chave de arquivo: não existe renomear.
|
|
14
|
+
//
|
|
15
|
+
// Esta view recebe TUDO por prop (nada de fetch aqui): é o que permite renderizá-la com um produto de
|
|
16
|
+
// mentira num teste, já que a loja local não fala com o catálogo. Quem busca é page.tsx.
|
|
17
|
+
//
|
|
18
|
+
// Server component: exportações NOMEADAS (`EditableSection`...), nunca o objeto `Editable` (README §4).
|
|
19
|
+
import { CaretRight } from "@phosphor-icons/react/dist/ssr";
|
|
20
|
+
import { EditableLink, EditableSection, EditableSections, EditableText } from "@/lib/editable";
|
|
21
|
+
import { PdpGallery } from "@/components/product/pdp/gallery";
|
|
22
|
+
import {
|
|
23
|
+
BuyBox,
|
|
24
|
+
type PdpBenefit,
|
|
25
|
+
type PdpRelatedOption,
|
|
26
|
+
type PdpSizeOption,
|
|
27
|
+
type PdpSubscription,
|
|
28
|
+
type PdpVariant,
|
|
29
|
+
} from "@/components/product/pdp/buy-box";
|
|
30
|
+
import { ProductTabs, FaqList, type KV } from "@/components/product/pdp/interactive";
|
|
31
|
+
import { TrustStrip, SecurityBar, ReviewsCard, QualidadeCard, QUALIDADE, type ReviewItem } from "@/components/product/pdp/sections";
|
|
32
|
+
import { Newsletter } from "@/components/product/pdp/newsletter";
|
|
33
|
+
import { CatalogGrid, CatalogCta, type CatalogItem } from "@/components/product/pdp/catalog-grid";
|
|
34
|
+
import type { PerguntaResposta } from "@/components/product/pdp/faq-modelo";
|
|
35
|
+
|
|
36
|
+
export interface PdpViewProps {
|
|
37
|
+
/** Dado do catálogo e do enriquecimento: nunca copy do molde. */
|
|
38
|
+
produto: {
|
|
39
|
+
id: string;
|
|
40
|
+
titulo: string;
|
|
41
|
+
imagens: string[];
|
|
42
|
+
videos: string[];
|
|
43
|
+
/** desconto REAL (compareAtPrice) para o selo da galeria; null = sem selo */
|
|
44
|
+
descontoPct: number | null;
|
|
45
|
+
descricaoCurta?: string | null;
|
|
46
|
+
esgotado: boolean;
|
|
47
|
+
minQty: number;
|
|
48
|
+
maxQty: number | null;
|
|
49
|
+
};
|
|
50
|
+
compra: {
|
|
51
|
+
variants: PdpVariant[];
|
|
52
|
+
benefits?: PdpBenefit[];
|
|
53
|
+
sizeOptions: PdpSizeOption[];
|
|
54
|
+
relatedTitle?: string;
|
|
55
|
+
relatedOptions: PdpRelatedOption[];
|
|
56
|
+
subscription: PdpSubscription | null;
|
|
57
|
+
/** nota e contagem REAIS (reviews do enriquecimento); sem elas o bloco de avaliação da buy box não aparece */
|
|
58
|
+
ratingAverage?: number;
|
|
59
|
+
ratingCount?: number;
|
|
60
|
+
};
|
|
61
|
+
detalhes: {
|
|
62
|
+
descHtml?: string | null;
|
|
63
|
+
infoHtml?: string | null;
|
|
64
|
+
specItems: KV[];
|
|
65
|
+
nutriItems: KV[];
|
|
66
|
+
nutriBase?: string | null;
|
|
67
|
+
usageSteps?: string[] | null;
|
|
68
|
+
};
|
|
69
|
+
/** null = sem avaliação real: o card não entra na página */
|
|
70
|
+
avaliacoes: { ratingCount: number; average: number; reviews: ReviewItem[] } | null;
|
|
71
|
+
/** FAQ do enriquecimento (por produto), dado do catálogo. As perguntas MODELO do molde entram sempre (faq-modelo.ts). */
|
|
72
|
+
faq?: readonly PerguntaResposta[];
|
|
73
|
+
catalogo: CatalogItem[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function PdpView({ produto, compra, detalhes, avaliacoes, faq, catalogo }: PdpViewProps) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="full-bleed store-layout bg-white text-[var(--store-ink)]">
|
|
79
|
+
<div className="mx-auto max-w-[1240px] px-4 pb-2 sm:px-6">
|
|
80
|
+
<EditableSections container="produto">
|
|
81
|
+
{/* FIXA: a galeria e o bloco de compra SÃO a página; reordenar e ocultar valem para o resto. */}
|
|
82
|
+
<EditableSection id="compra" kind="produto-em-destaque" label="Bloco de compra" fixed>
|
|
83
|
+
{/* caminho de navegação: os dois links são copy de navegação da marca; o nome do produto é dado */}
|
|
84
|
+
<div className="flex items-center gap-2 pt-4 text-[13px] font-medium text-[var(--store-muted)]">
|
|
85
|
+
<EditableLink
|
|
86
|
+
path="trilha.inicio"
|
|
87
|
+
fallback={{ href: "/", label: "Início" }}
|
|
88
|
+
label="Caminho: link da página inicial"
|
|
89
|
+
className="no-underline hover:text-[var(--store-ink)]"
|
|
90
|
+
/>
|
|
91
|
+
<CaretRight className="text-[11px]" />
|
|
92
|
+
<EditableLink
|
|
93
|
+
path="trilha.produtos"
|
|
94
|
+
fallback={{ href: "/produtos", label: "Produtos" }}
|
|
95
|
+
label="Caminho: link da lista de produtos"
|
|
96
|
+
className="no-underline hover:text-[var(--store-ink)]"
|
|
97
|
+
/>
|
|
98
|
+
<CaretRight className="text-[11px]" />
|
|
99
|
+
<span data-editor-ignore className="font-semibold text-[var(--store-ink)] line-clamp-1">{produto.titulo}</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="grid grid-cols-1 items-start gap-7 pt-4 lg:grid-cols-[1.05fr_0.95fr] lg:gap-12">
|
|
103
|
+
<PdpGallery images={produto.imagens} videos={produto.videos} title={produto.titulo} discountPct={produto.descontoPct} />
|
|
104
|
+
<BuyBox
|
|
105
|
+
ratingAverage={compra.ratingAverage}
|
|
106
|
+
ratingCount={compra.ratingCount}
|
|
107
|
+
productId={produto.id}
|
|
108
|
+
title={produto.titulo}
|
|
109
|
+
shortDescription={produto.descricaoCurta}
|
|
110
|
+
imageUrl={produto.imagens[0]}
|
|
111
|
+
variants={compra.variants}
|
|
112
|
+
benefits={compra.benefits}
|
|
113
|
+
sizeOptions={compra.sizeOptions}
|
|
114
|
+
relatedTitle={compra.relatedTitle}
|
|
115
|
+
relatedOptions={compra.relatedOptions}
|
|
116
|
+
subscription={compra.subscription}
|
|
117
|
+
minQty={produto.minQty}
|
|
118
|
+
maxQty={produto.maxQty}
|
|
119
|
+
isSoldOut={produto.esgotado}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</EditableSection>
|
|
123
|
+
|
|
124
|
+
{/* Abaixo do frame principal: confiança, conteúdo do produto, qualidade + FAQ, catálogo, newsletter. */}
|
|
125
|
+
<EditableSection id="confianca" kind="beneficios" label="Faixa de confiança">
|
|
126
|
+
<div className="mt-10"><TrustStrip /></div>
|
|
127
|
+
</EditableSection>
|
|
128
|
+
|
|
129
|
+
<EditableSection id="seguranca" kind="beneficios" label="Barra de compra segura e pagamento">
|
|
130
|
+
<div className="mt-5"><SecurityBar /></div>
|
|
131
|
+
</EditableSection>
|
|
132
|
+
|
|
133
|
+
{/* descrição, ficha, tabela nutricional e modo de uso: TUDO dado do produto; os rótulos das abas são interface */}
|
|
134
|
+
<EditableSection id="detalhes" kind="outro" label="Descrição e ficha do produto">
|
|
135
|
+
<div className="mt-12" data-editor-ignore>
|
|
136
|
+
<ProductTabs
|
|
137
|
+
descHtml={detalhes.descHtml}
|
|
138
|
+
infoHtml={detalhes.infoHtml}
|
|
139
|
+
specItems={detalhes.specItems}
|
|
140
|
+
nutriItems={detalhes.nutriItems}
|
|
141
|
+
nutriBase={detalhes.nutriBase}
|
|
142
|
+
usageSteps={detalhes.usageSteps}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
</EditableSection>
|
|
146
|
+
|
|
147
|
+
{/* avaliações só entram quando existem de verdade */}
|
|
148
|
+
{avaliacoes && (
|
|
149
|
+
<EditableSection id="avaliacoes" kind="depoimentos" label="Avaliações">
|
|
150
|
+
<div id="avaliacoes" className="mt-12">
|
|
151
|
+
<ReviewsCard ratingCount={avaliacoes.ratingCount} average={avaliacoes.average} reviews={avaliacoes.reviews} />
|
|
152
|
+
</div>
|
|
153
|
+
</EditableSection>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* qualidade + FAQ dividem UMA linha da grade. Os dois cartões são ITENS de uma lista aninhada: cada
|
|
157
|
+
um oculta e reordena por si, e cada um hospeda a própria lista (atributos de qualidade,
|
|
158
|
+
perguntas) num container só dele. A grade é auto-fit: com um cartão só (qualidade vazia ou
|
|
159
|
+
oculta pelo lojista) o FAQ ocupa a linha inteira em vez de deixar uma coluna vazia. */}
|
|
160
|
+
<EditableSection id="qualidade-faq" kind="perguntas-frequentes" label="Qualidade e perguntas frequentes">
|
|
161
|
+
<div className="mt-12 grid items-start gap-[22px] lg:grid-cols-[repeat(auto-fit,minmax(320px,1fr))]">
|
|
162
|
+
<EditableSections nested>
|
|
163
|
+
{/* o cartão de qualidade só existe com atributos declarados (sections.tsx, QUALIDADE) */}
|
|
164
|
+
{QUALIDADE.length > 0 && (
|
|
165
|
+
<EditableSection item id="qualidade" label="Qualidade do produto">
|
|
166
|
+
<QualidadeCard />
|
|
167
|
+
</EditableSection>
|
|
168
|
+
)}
|
|
169
|
+
<EditableSection item id="faq" label="Perguntas frequentes">
|
|
170
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-[28px]">
|
|
171
|
+
<EditableText
|
|
172
|
+
as="h2"
|
|
173
|
+
path="titulo"
|
|
174
|
+
fallback="Perguntas frequentes"
|
|
175
|
+
label="Título das perguntas frequentes"
|
|
176
|
+
className="font-display mb-[22px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]"
|
|
177
|
+
/>
|
|
178
|
+
<FaqList items={faq} />
|
|
179
|
+
</div>
|
|
180
|
+
</EditableSection>
|
|
181
|
+
</EditableSections>
|
|
182
|
+
</div>
|
|
183
|
+
</EditableSection>
|
|
184
|
+
|
|
185
|
+
{/* catálogo: chapéu, título, frase e link são copy do molde; os produtos da grade são dado */}
|
|
186
|
+
{catalogo.length > 0 && (
|
|
187
|
+
<EditableSection id="catalogo" kind="vitrine-de-produtos" label="Mais produtos do catálogo">
|
|
188
|
+
<div className="mt-[52px]">
|
|
189
|
+
<div className="mb-1.5 flex items-end justify-between">
|
|
190
|
+
<div>
|
|
191
|
+
<EditableText
|
|
192
|
+
as="div"
|
|
193
|
+
path="chapeu"
|
|
194
|
+
fallback="CATÁLOGO"
|
|
195
|
+
label="Chapéu do catálogo"
|
|
196
|
+
className="text-xs font-extrabold tracking-[1.5px] text-[var(--store-primary,#18181B)]"
|
|
197
|
+
/>
|
|
198
|
+
<EditableText
|
|
199
|
+
as="h2"
|
|
200
|
+
path="titulo"
|
|
201
|
+
fallback="Explore mais do catálogo"
|
|
202
|
+
label="Título do catálogo"
|
|
203
|
+
className="font-display mt-1.5 text-[26px] font-extrabold"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
<CatalogCta />
|
|
207
|
+
</div>
|
|
208
|
+
<EditableText
|
|
209
|
+
as="p"
|
|
210
|
+
path="texto"
|
|
211
|
+
fallback="Clientes que viram este produto também levam:"
|
|
212
|
+
label="Frase de apoio do catálogo"
|
|
213
|
+
className="mb-[22px] mt-1 text-sm text-[var(--store-muted)]"
|
|
214
|
+
/>
|
|
215
|
+
<CatalogGrid items={catalogo} />
|
|
216
|
+
</div>
|
|
217
|
+
</EditableSection>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
<EditableSection id="newsletter" kind="newsletter" label="Newsletter">
|
|
221
|
+
<div className="mt-12"><Newsletter /></div>
|
|
222
|
+
</EditableSection>
|
|
223
|
+
</EditableSections>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Seções de recomendação da PDP com PRODUTOS REAIS do catálogo e add-to-cart funcional.
|
|
4
|
+
// Substitui os antigos Bundles/Combos com preços fictícios e botões sem ação.
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import Image from "next/image";
|
|
8
|
+
import { Plus, Spinner, ShoppingCartSimple, ArrowRight } from "@phosphor-icons/react/dist/ssr";
|
|
9
|
+
import { useCart } from "@/components/cart/cart-provider";
|
|
10
|
+
import { formatBRL } from "@/lib/format";
|
|
11
|
+
import type { CatalogItem } from "@/components/product/pdp/catalog-grid";
|
|
12
|
+
|
|
13
|
+
const canAdd = (p: CatalogItem) => !!p.productId && !!p.variantId && p.price != null;
|
|
14
|
+
|
|
15
|
+
function useQuickAdd() {
|
|
16
|
+
const { add } = useCart();
|
|
17
|
+
const [busyId, setBusyId] = React.useState<string | null>(null);
|
|
18
|
+
const addOne = React.useCallback(async (p: CatalogItem) => {
|
|
19
|
+
if (!canAdd(p)) return;
|
|
20
|
+
setBusyId(p.variantId!);
|
|
21
|
+
await add({ productId: p.productId!, variantId: p.variantId!, price: p.price ?? 0, quantity: 1, thumbnail: p.imageUrl ?? undefined, title: p.title });
|
|
22
|
+
setBusyId(null);
|
|
23
|
+
}, [add]);
|
|
24
|
+
return { addOne, busyId };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Thumb({ item, size = 84 }: { item: CatalogItem; size?: number }) {
|
|
28
|
+
return (
|
|
29
|
+
<span className="relative flex items-center justify-center overflow-hidden rounded-md border border-[var(--store-line)] bg-[var(--store-line)]" style={{ width: size, height: size }}>
|
|
30
|
+
{item.imageUrl ? (
|
|
31
|
+
<Image src={item.imageUrl} alt={item.title} fill sizes={`${size}px`} className="object-contain p-0.5" />
|
|
32
|
+
) : (
|
|
33
|
+
<span className="text-[10px] text-[var(--store-faint)]">sem foto</span>
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** "Compre junto" (real): N produtos reais + adicionar todos de uma vez. Sem desconto fabricado. */
|
|
40
|
+
export function BuyTogether({ items }: { items: CatalogItem[] }) {
|
|
41
|
+
const products = items.filter(canAdd).slice(0, 3);
|
|
42
|
+
const { addOne } = useQuickAdd();
|
|
43
|
+
const [busyAll, setBusyAll] = React.useState(false);
|
|
44
|
+
if (products.length < 2) return null;
|
|
45
|
+
const sum = products.reduce((s, p) => s + (p.price ?? 0), 0);
|
|
46
|
+
|
|
47
|
+
async function addAll() {
|
|
48
|
+
setBusyAll(true);
|
|
49
|
+
for (const p of products) await addOne(p);
|
|
50
|
+
setBusyAll(false);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-[26px]">
|
|
55
|
+
<h2 className="font-display mb-5 text-[22px] font-extrabold text-[var(--store-primary,#18181B)]">Compre junto</h2>
|
|
56
|
+
<div className="flex flex-wrap items-start gap-2 max-sm:justify-center max-sm:gap-3.5">
|
|
57
|
+
{products.map((p, i) => (
|
|
58
|
+
<div key={p.slug} className="contents">
|
|
59
|
+
<div className="flex w-[100px] shrink-0 flex-col items-center gap-1.5">
|
|
60
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`}><Thumb item={p} /></Link>
|
|
61
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`} className="line-clamp-2 text-center text-xs font-bold leading-tight text-[var(--store-ink)] no-underline">{p.title}</Link>
|
|
62
|
+
<div className="text-[13px] font-extrabold text-[var(--store-ink)]">{p.displayPrice}</div>
|
|
63
|
+
</div>
|
|
64
|
+
{i < products.length - 1 && (
|
|
65
|
+
<span className="mt-9 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--store-primary,#18181B)] text-white"><Plus weight="bold" className="text-[15px]" /></span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
<span className="mt-9 shrink-0 text-[22px] font-bold text-[var(--store-faint)]">=</span>
|
|
70
|
+
<div className="flex flex-1 flex-col items-center gap-1.5 pt-0.5 text-center">
|
|
71
|
+
<div className="text-[13px] text-[var(--store-muted)]">Os {products.length} por</div>
|
|
72
|
+
<div className="font-display text-[26px] font-extrabold leading-none text-[var(--store-ink)]">{formatBRL(sum)}</div>
|
|
73
|
+
<button type="button" onClick={addAll} disabled={busyAll}
|
|
74
|
+
className="font-display mt-1 flex cursor-pointer items-center gap-1.5 rounded-md bg-[var(--store-primary,#18181B)] px-5 py-3 text-[13px] font-bold leading-tight text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
|
|
75
|
+
{busyAll ? <Spinner className="animate-spin" /> : <ShoppingCartSimple weight="bold" />}Adicionar todos
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** "Frequentemente comprados juntos" (real): produtos reais com quick-add individual. */
|
|
84
|
+
export function FrequentlyBought({ items }: { items: CatalogItem[] }) {
|
|
85
|
+
const products = items.filter(canAdd).slice(0, 3);
|
|
86
|
+
const { addOne, busyId } = useQuickAdd();
|
|
87
|
+
if (products.length === 0) return null;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-[26px]">
|
|
91
|
+
<h2 className="font-display mb-5 text-[22px] font-extrabold text-[var(--store-ink)]">Frequentemente comprados juntos</h2>
|
|
92
|
+
<div className="flex items-start gap-[18px]">
|
|
93
|
+
{products.map((p) => (
|
|
94
|
+
<div key={p.slug} className="flex flex-1 flex-col items-center gap-1.5">
|
|
95
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`}><Thumb item={p} size={96} /></Link>
|
|
96
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`} className="line-clamp-2 text-center text-xs font-bold leading-tight text-[var(--store-ink)] no-underline">{p.title}</Link>
|
|
97
|
+
<div className="text-[13px] font-extrabold text-[var(--store-ink)]">{p.displayPrice}</div>
|
|
98
|
+
<button type="button" onClick={() => addOne(p)} disabled={busyId === p.variantId} aria-label={`Adicionar ${p.title}`}
|
|
99
|
+
className="flex h-[34px] w-[34px] items-center justify-center rounded-full bg-[var(--store-primary,#18181B)] text-white shadow-[var(--store-shadow-cta-sm)] transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
|
|
100
|
+
{busyId === p.variantId ? <Spinner className="animate-spin text-base" /> : <Plus weight="bold" className="text-base" />}
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Grade de "kits sugeridos" (real): produtos reais com quick-add. Sem % OFF fabricado. */
|
|
110
|
+
export function SuggestedKits({ items }: { items: CatalogItem[] }) {
|
|
111
|
+
const products = items.filter(canAdd).slice(0, 3);
|
|
112
|
+
const { addOne, busyId } = useQuickAdd();
|
|
113
|
+
if (products.length === 0) return null;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<div className="mb-[18px] flex items-end justify-between">
|
|
118
|
+
<div>
|
|
119
|
+
<h2 className="font-display text-2xl font-extrabold">Leve também</h2>
|
|
120
|
+
<p className="mt-1 text-sm text-[var(--store-muted)]">Você também pode gostar.</p>
|
|
121
|
+
</div>
|
|
122
|
+
<Link href="/produtos" className="flex items-center gap-1.5 text-sm font-bold text-[var(--store-primary,#18181B)] no-underline max-sm:hidden">
|
|
123
|
+
Ver todos os produtos <ArrowRight weight="bold" />
|
|
124
|
+
</Link>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="grid gap-[18px] sm:grid-cols-2 lg:grid-cols-3">
|
|
127
|
+
{products.map((p) => (
|
|
128
|
+
<div key={p.slug} className="flex flex-col rounded-2xl border border-[var(--store-line)] bg-white p-[18px]">
|
|
129
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`} className="flex justify-center py-2">
|
|
130
|
+
<Thumb item={p} size={120} />
|
|
131
|
+
</Link>
|
|
132
|
+
<Link href={`/produto/${encodeURIComponent(p.slug)}`} className="mt-2 line-clamp-2 text-base font-bold text-[var(--store-ink)] no-underline">{p.title}</Link>
|
|
133
|
+
{p.weight && <div className="mt-0.5 text-[12.5px] text-[var(--store-muted)]">{p.weight}</div>}
|
|
134
|
+
<div className="font-display mt-2 text-[22px] font-extrabold text-[var(--store-primary,#18181B)]">{p.displayPrice}</div>
|
|
135
|
+
<button type="button" onClick={() => addOne(p)} disabled={busyId === p.variantId}
|
|
136
|
+
className="font-display mt-3.5 flex cursor-pointer items-center justify-center gap-1.5 rounded-md bg-[var(--store-primary,#18181B)] py-3 text-[13.5px] font-bold text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)] disabled:opacity-60">
|
|
137
|
+
{busyId === p.variantId ? <Spinner className="animate-spin" /> : <Plus weight="bold" />}Adicionar
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Truck, Lightning, LockSimple, SealCheck, Star } from "@phosphor-icons/react/dist/ssr";
|
|
2
|
+
import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
|
|
3
|
+
// EDITOR: este arquivo é server component (sem "use client"): exportações NOMEADAS, nunca `Editable.*` (README §4).
|
|
4
|
+
import { EditableIcon, EditableSection, EditableSections, EditableText } from "@/lib/editable";
|
|
5
|
+
import { PaymentChips } from "@/components/product/pdp/payment-chips";
|
|
6
|
+
import { FREE_SHIPPING_THRESHOLD } from "@/lib/store-config";
|
|
7
|
+
|
|
8
|
+
// Os selos são uma LISTA editável (README do editor §3: `Sections nested` + `Section item`), com id por
|
|
9
|
+
// PAPEL e não por posição: o selo de frete só existe com FREE_SHIPPING_THRESHOLD, e um id posicional
|
|
10
|
+
// deslizaria (o texto que o lojista escreveu no selo 2 apareceria no selo 1) quando a regra mudasse.
|
|
11
|
+
// Selo de frete grátis só entra quando há regra real configurada em lib/store-config.
|
|
12
|
+
//
|
|
13
|
+
// Só o que o template consegue afirmar: SSL é fato; o prazo é calculado pela cotação real; 7 dias é o
|
|
14
|
+
// art. 49 do CDC, vale para toda loja online no Brasil. "Expressa em 1 a 2 dias" e "devolução em 30
|
|
15
|
+
// dias" eram inventados, e o segundo contradizia a própria página /devolucoes da loja.
|
|
16
|
+
const TRUST = [
|
|
17
|
+
...(FREE_SHIPPING_THRESHOLD != null
|
|
18
|
+
? [{ id: "frete", label: "Selo de frete grátis", icon: Truck, title: `Frete grátis acima de R$${FREE_SHIPPING_THRESHOLD}`, sub: "para todo o Brasil" }]
|
|
19
|
+
: []),
|
|
20
|
+
{ id: "entrega", label: "Selo de entrega", icon: Lightning, title: "Entrega para todo o Brasil", sub: "prazo calculado no carrinho" },
|
|
21
|
+
{ id: "seguranca", label: "Selo de compra segura", icon: LockSimple, title: "Compra 100% segura", sub: "criptografia SSL" },
|
|
22
|
+
{ id: "arrependimento", label: "Selo de devolução", icon: SealCheck, title: "Troca em 7 dias", sub: "arrependimento garantido por lei (CDC)" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function TrustStrip() {
|
|
26
|
+
return (
|
|
27
|
+
<div className={`grid grid-cols-2 rounded-2xl border border-[var(--store-line)] bg-white ${TRUST.length === 4 ? "md:grid-cols-4" : "md:grid-cols-3"}`}>
|
|
28
|
+
<EditableSections nested>
|
|
29
|
+
{TRUST.map((t, i) => (
|
|
30
|
+
<EditableSection key={t.id} item id={t.id} label={t.label}>
|
|
31
|
+
{/* o invólucro da seção é display:contents: este div continua sendo o item da grade */}
|
|
32
|
+
<div className="flex items-center gap-3.5 px-[22px] py-5" style={{ borderRight: i < TRUST.length - 1 ? "1px solid var(--store-surface-2)" : "none" }}>
|
|
33
|
+
<EditableIcon path="icone" label="Ícone do selo" size={26} className="text-[26px] text-[var(--store-primary,#18181B)]">
|
|
34
|
+
<t.icon />
|
|
35
|
+
</EditableIcon>
|
|
36
|
+
<div>
|
|
37
|
+
<EditableText as="div" path="titulo" fallback={t.title} label="Título do selo" className="text-[13.5px] font-bold text-[var(--store-ink)]" />
|
|
38
|
+
<EditableText as="div" path="texto" fallback={t.sub} label="Texto do selo" className="mt-0.5 text-xs text-[var(--store-muted)]" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</EditableSection>
|
|
42
|
+
))}
|
|
43
|
+
</EditableSections>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function SecurityBar() {
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-wrap items-center justify-between gap-6 rounded-xl bg-[var(--store-chrome-bg)] px-8 py-[22px]">
|
|
51
|
+
<div className="flex items-center gap-[18px]">
|
|
52
|
+
<span className="flex h-[52px] w-[52px] shrink-0 items-center justify-center rounded-full bg-[var(--store-primary,#18181B)]">
|
|
53
|
+
<EditableIcon path="icone" label="Ícone de compra segura" size={25} className="text-[25px] text-white">
|
|
54
|
+
<LockSimple weight="fill" />
|
|
55
|
+
</EditableIcon>
|
|
56
|
+
</span>
|
|
57
|
+
<div>
|
|
58
|
+
<EditableText as="div" path="titulo" fallback="Compra 100% Segura" label="Título" className="font-display text-lg font-bold text-white" />
|
|
59
|
+
<EditableText as="div" path="texto" fallback="Seus dados protegidos do início ao fim." label="Texto de apoio" className="mt-0.5 text-[13.5px] text-[var(--store-chrome-muted)]" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex items-center gap-[22px]">
|
|
63
|
+
<EditableText as="span" path="pagamento" fallback="Pague como preferir" label="Texto ao lado das bandeiras" className="font-display text-[15px] font-semibold text-white max-md:hidden" />
|
|
64
|
+
<span className="h-[34px] w-px bg-white/20 max-md:hidden" />
|
|
65
|
+
{/* bandeiras de pagamento: selo de terceiro, fora do editor (o próprio componente se marca) */}
|
|
66
|
+
<PaymentChips size="md" />
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ReviewItem { author: string; stars: number; comment: string; verified?: boolean }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Avaliações do produto: SÓ com dado real. Sem reviews, não renderiza (o chamador também
|
|
76
|
+
* esconde o card). Não existe nota default, contagem default nem depoimento default: a
|
|
77
|
+
* foundation publicava "4,9 · 25.347 avaliações" e "Cliente A/B/C" com selo de verificação em
|
|
78
|
+
* loja recém-aberta, e isso foi ao ar. A distribuição por estrela é calculada das reviews.
|
|
79
|
+
*
|
|
80
|
+
* EDITOR: só o TÍTULO é copy do molde. Nota, contagem, barras e depoimentos são dado das avaliações e
|
|
81
|
+
* ficam fora (`data-editor-ignore`, README §8).
|
|
82
|
+
*/
|
|
83
|
+
export function ReviewsCard({ ratingCount, average, reviews }: {
|
|
84
|
+
ratingCount: number;
|
|
85
|
+
average: number;
|
|
86
|
+
reviews: ReviewItem[];
|
|
87
|
+
}) {
|
|
88
|
+
if (!reviews.length || !ratingCount) return null;
|
|
89
|
+
const avg = average.toFixed(1).replace(".", ",");
|
|
90
|
+
const dist = [5, 4, 3, 2, 1].map((n) => {
|
|
91
|
+
const q = reviews.filter((r) => Math.round(r.stars) === n).length;
|
|
92
|
+
return { n: String(n), pct: `${Math.round((q / reviews.length) * 100)}%` };
|
|
93
|
+
});
|
|
94
|
+
const list = reviews.slice(0, 6).map((r) => ({ initial: (r.author.trim()[0] || "?").toUpperCase(), name: r.author, quip: r.comment, stars: r.stars, verified: r.verified }));
|
|
95
|
+
return (
|
|
96
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-7 p-[28px]">
|
|
97
|
+
<EditableText as="h2" path="titulo" fallback="Quem comprou, recomenda" label="Título das avaliações" className="font-display mb-[18px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]" />
|
|
98
|
+
<div className="flex items-center gap-3.5" data-editor-ignore>
|
|
99
|
+
<span className="font-display text-[44px] font-extrabold leading-none text-[var(--store-ink)]">{avg}</span>
|
|
100
|
+
<span className="flex text-2xl text-[var(--store-cta-dark)]" aria-label={`${avg} de 5`}>{[1, 2, 3, 4, 5].map((i) => <Star key={i} weight={average >= i - 0.25 ? "fill" : "regular"} />)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="mt-2.5 mb-[18px] text-[13px] text-[var(--store-muted)]" data-editor-ignore>Baseado em {ratingCount.toLocaleString("pt-BR")} {ratingCount === 1 ? "avaliação" : "avaliações"}</div>
|
|
103
|
+
<div className="flex flex-col gap-2.5" data-editor-ignore>
|
|
104
|
+
{dist.map((r) => (
|
|
105
|
+
<div key={r.n} className="flex items-center gap-2.5 text-[12.5px] text-[var(--store-muted)]">
|
|
106
|
+
<span className="w-2.5">{r.n}</span>
|
|
107
|
+
<Star weight="fill" className="text-[11px] text-[var(--store-cta-dark)]" />
|
|
108
|
+
<div className="h-[7px] flex-1 overflow-hidden rounded-full bg-[var(--store-surface-2)]"><div className="h-full rounded-full bg-[var(--store-cta-dark)]" style={{ width: r.pct }} /></div>
|
|
109
|
+
<span className="w-[34px] text-right">{r.pct}</span>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="my-[22px] h-px bg-[var(--store-surface-2)]" />
|
|
114
|
+
<div className="flex flex-col gap-4" data-editor-ignore>
|
|
115
|
+
{list.map((rv, i) => (
|
|
116
|
+
<div key={i} className="flex items-start gap-2.5">
|
|
117
|
+
<div className="font-display flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-full bg-[var(--store-chrome-bg)] text-sm font-extrabold text-white">{rv.initial}</div>
|
|
118
|
+
<div className="min-w-0 flex-1">
|
|
119
|
+
<div className="flex items-center gap-1.5">
|
|
120
|
+
<span className="text-[13.5px] font-bold">{rv.name}</span>
|
|
121
|
+
<span className="flex text-[10px] text-[var(--store-cta-dark)]">{[1, 2, 3, 4, 5].map((i) => <Star key={i} weight={rv.stars >= i - 0.25 ? "fill" : "regular"} />)}</span>
|
|
122
|
+
{rv.verified && <span className="text-[11px] font-semibold text-[var(--store-primary,#18181B)]">· compra verificada</span>}
|
|
123
|
+
</div>
|
|
124
|
+
<div className="mt-0.5 text-[13px] leading-snug text-[var(--store-ink-2)]">{rv.quip}</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* OS ATRIBUTOS DE QUALIDADE DO PRODUTO: o que a marca afirma sobre o que vende (feito à mão, produção em
|
|
135
|
+
* pequenos lotes, garantia). Nasce VAZIO de propósito: a foundation não sabe o que o produto tem, e um
|
|
136
|
+
* atributo plausível inventado aqui iria ao ar como afirmação da marca (foi assim na v0.15.6, e o cartão
|
|
137
|
+
* saiu). Quem preenche é o briefing, só com o que a marca sustenta. Sem itens, o cartão não renderiza e
|
|
138
|
+
* o FAQ ocupa a linha inteira.
|
|
139
|
+
*
|
|
140
|
+
* EDITOR: a PDP é um MOLDE (um valor para todos os produtos), e estes atributos são o que o lojista muda
|
|
141
|
+
* quando o catálogo cresce; por isso cada um é item de lista editável (ícone, título e texto), com o id
|
|
142
|
+
* posicional da foundation (`item-1`, `item-2`...). Apagar um item do meio NO CÓDIGO desliza os ids
|
|
143
|
+
* seguintes (README do editor, §6): prefira ocultar pelo painel.
|
|
144
|
+
*/
|
|
145
|
+
export const QUALIDADE: readonly { icon: PhosphorIcon; title: string; sub: string }[] = [];
|
|
146
|
+
|
|
147
|
+
export function QualidadeCard() {
|
|
148
|
+
if (QUALIDADE.length === 0) return null;
|
|
149
|
+
return (
|
|
150
|
+
<div className="rounded-xl border border-[var(--store-line)] bg-white p-7 p-[28px]">
|
|
151
|
+
<EditableText as="h2" path="titulo" fallback="Qualidade que você sente" label="Título do bloco de qualidade" className="font-display mb-[22px] text-[21px] font-extrabold italic leading-tight text-[var(--store-primary,#18181B)]" />
|
|
152
|
+
<div className="flex flex-col gap-5">
|
|
153
|
+
<EditableSections nested>
|
|
154
|
+
{QUALIDADE.map((q, i) => (
|
|
155
|
+
<EditableSection key={q.title} item id={`item-${i + 1}`} label={`Atributo ${i + 1}`}>
|
|
156
|
+
<div className="flex items-start gap-3.5">
|
|
157
|
+
<span className="flex h-[54px] w-[54px] shrink-0 items-center justify-center rounded-xl bg-[var(--store-primary-soft,#F1F1F3)]">
|
|
158
|
+
<EditableIcon path="icone" label="Ícone do atributo" size={26} className="text-[26px] text-[var(--store-primary,#18181B)]">
|
|
159
|
+
<q.icon />
|
|
160
|
+
</EditableIcon>
|
|
161
|
+
</span>
|
|
162
|
+
<div>
|
|
163
|
+
<EditableText as="div" path="titulo" fallback={q.title} label="Título do atributo" className="text-[15px] font-bold text-[var(--store-ink)]" />
|
|
164
|
+
<EditableText as="div" path="texto" fallback={q.sub} label="Texto do atributo" className="mt-0.5 text-[13.5px] leading-snug text-[var(--store-muted)]" />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</EditableSection>
|
|
168
|
+
))}
|
|
169
|
+
</EditableSections>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Minus, Plus } from "@/lib/icons";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
// Stepper de quantidade respeitando min/max do produto (doc 02).
|
|
8
|
+
export function QuantityStepper({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
min = 1,
|
|
12
|
+
max,
|
|
13
|
+
disabled,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
value: number;
|
|
17
|
+
onChange: (v: number) => void;
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number | null;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}) {
|
|
23
|
+
const dec = () => onChange(Math.max(min, value - 1));
|
|
24
|
+
const inc = () => onChange(max ? Math.min(max, value + 1) : value + 1);
|
|
25
|
+
return (
|
|
26
|
+
<div className={cn("inline-flex items-center rounded-lg border", className)} role="group" aria-label="Quantidade">
|
|
27
|
+
<Button type="button" variant="ghost" size="icon-sm" onClick={dec} disabled={disabled || value <= min} aria-label="Diminuir">
|
|
28
|
+
<Minus />
|
|
29
|
+
</Button>
|
|
30
|
+
<span className="w-9 text-center text-sm tabular-nums" aria-live="polite">{value}</span>
|
|
31
|
+
<Button type="button" variant="ghost" size="icon-sm" onClick={inc} disabled={disabled || (max != null && value >= max)} aria-label="Aumentar">
|
|
32
|
+
<Plus />
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr";
|
|
6
|
+
|
|
7
|
+
export function SearchBox({ defaultValue = "", className }: { defaultValue?: string; className?: string }) {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [q, setQ] = React.useState(defaultValue);
|
|
10
|
+
return (
|
|
11
|
+
<form
|
|
12
|
+
role="search"
|
|
13
|
+
className={className}
|
|
14
|
+
onSubmit={(e) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
const term = q.trim();
|
|
17
|
+
router.push(term ? `/busca?q=${encodeURIComponent(term)}` : "/produtos");
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<div className="flex items-center gap-2 rounded-xl border-[1.5px] border-[var(--store-line-2)] bg-white px-3.5 transition-colors focus-within:border-[var(--store-primary,#18181B)]">
|
|
21
|
+
<MagnifyingGlass weight="bold" className="shrink-0 text-[19px] text-[var(--store-muted)]" />
|
|
22
|
+
<input
|
|
23
|
+
type="search"
|
|
24
|
+
name="q"
|
|
25
|
+
value={q}
|
|
26
|
+
onChange={(e) => setQ(e.target.value)}
|
|
27
|
+
placeholder="Buscar produtos..."
|
|
28
|
+
aria-label="Buscar produtos"
|
|
29
|
+
className="h-12 min-w-0 flex-1 bg-transparent text-[15px] font-medium text-[var(--store-ink)] outline-none placeholder:text-[var(--store-faint)]"
|
|
30
|
+
/>
|
|
31
|
+
<button type="submit" className="font-display shrink-0 cursor-pointer rounded-lg bg-[var(--store-primary,#18181B)] px-4 py-2 text-[13px] font-bold text-white transition-colors hover:bg-[var(--store-primary-dark,#09090B)]">
|
|
32
|
+
Buscar
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</form>
|
|
36
|
+
);
|
|
37
|
+
}
|