@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.
Files changed (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,372 @@
1
+ import type { Metadata } from "next";
2
+ import { notFound } from "next/navigation";
3
+ import { getCatalog, getProductBySlug, getShopData } from "@/lib/queries";
4
+ import { sanitize } from "@/lib/sanitize";
5
+ import { resolveProductPrice, resolveVariantPrice } from "@/lib/format";
6
+ import { RELATED_GROUPS } from "@/lib/store-config";
7
+ import { getEnrichmentForProduct, getEnrichmentByName, parseSize, reviewStats, type ProductEnrichment } from "@/lib/enrichment";
8
+ import { DataLayerReady } from "@/components/analytics/data-layer-ready";
9
+ import type { PdpVariant, PdpSizeOption, PdpRelatedOption, PdpBenefit } from "@/components/product/pdp/buy-box";
10
+ import type { CatalogItem } from "@/components/product/pdp/catalog-grid";
11
+ // A PDP é um MOLDE: a view recebe tudo por prop (components/product/pdp/pdp-view.tsx) e é ela que
12
+ // declara o container `produto` e as seções. Aqui só se busca e se prepara o dado.
13
+ import { PdpView } from "@/components/product/pdp/pdp-view";
14
+ import { faqNaTela } from "@/components/product/pdp/faq-modelo";
15
+ import { mockupOr } from "@/lib/mockup";
16
+ import { hasUnboxCredentials } from "@/lib/config";
17
+ import { ldJson } from "@/lib/json-ld";
18
+ // EDITOR: o documento publicado, para o FAQPage dizer o que o accordion mostra (ver `faqNaTela`)
19
+ import { getPublishedContent } from "@/lib/editable/server";
20
+
21
+ // DADO AUSENTE NA FICHA. Era um travessão, que na tela é um sinal e não uma informação, e a casa
22
+ // proibiu travessão em texto de tela. "não informado" diz o que aconteceu: o cadastro não trouxe o dado.
23
+ const NAO_INFORMADO = "não informado";
24
+
25
+ export const revalidate = 300;
26
+
27
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
28
+ // Mesmo literal do app/layout.tsx: o create-unbox-store reescreve "Minha Loja" nos dois
29
+ // arquivos com o nome real da loja.
30
+ const SITE_NAME = "Minha Loja";
31
+
32
+ export async function generateStaticParams() {
33
+ // Aqui (e só aqui) a falha NÃO relança: sem a lista, as PDPs saem sob demanda via ISR em vez
34
+ // de derrubar o build inteiro. Com credenciais, fica registrado no log do build.
35
+ const catalog = await getCatalog({ first: 100 }).catch((e) => {
36
+ if (hasUnboxCredentials) console.error("[unbox] generateStaticParams(produto): catálogo indisponível, PDPs serão geradas sob demanda:", e?.message ?? e);
37
+ return { nodes: [] as any[] };
38
+ });
39
+ return (catalog.nodes ?? [])
40
+ .map((n: any) => ({ productSlug: (n.product?.slug ?? n.slug) as string | undefined }))
41
+ .filter((p): p is { productSlug: string } => !!p.productSlug);
42
+ }
43
+
44
+ export async function generateMetadata({ params }: { params: Promise<{ productSlug: string }> }): Promise<Metadata> {
45
+ const { productSlug } = await params;
46
+ const wrapper = await mockupOr(getProductBySlug(decodeURIComponent(productSlug)), null, "produto/getProductBySlug");
47
+ const p = wrapper?.product;
48
+ if (!p) return { title: "Produto não encontrado", robots: { index: false } };
49
+ // Canonical é SEMPRE a URL desta loja. `publishedUrl` vem do painel e pode apontar pro
50
+ // domínio antigo/hospedado: canonical cruzado entrega o ranking da PDP pra outro site.
51
+ const canonical = `/produto/${encodeURIComponent(productSlug)}`;
52
+ return {
53
+ title: p.pageTitle || p.title,
54
+ description: p.metaDescription || wrapper.shortDescription || undefined,
55
+ alternates: { canonical },
56
+ robots: { index: p.isVisible !== false },
57
+ // Metadata do App Router substitui o objeto openGraph INTEIRO do layout pai, não
58
+ // faz merge profundo: declarar só title/description/images aqui apagava siteName e
59
+ // locale, e a loja publicava card social sem identificação. Os campos do layout
60
+ // precisam ser repetidos.
61
+ openGraph: {
62
+ type: "website",
63
+ locale: "pt_BR",
64
+ siteName: SITE_NAME,
65
+ title: p.pageTitle || p.title,
66
+ description: p.metaDescription || wrapper.shortDescription || undefined,
67
+ images: p.imageUrls?.length ? [p.imageUrls[0]] : undefined,
68
+ },
69
+ };
70
+ }
71
+
72
+ function freqNote(periodicity?: string, interval?: number): string {
73
+ const n = interval ?? 1;
74
+ const unit = { DAY: "dia", WEEK: "semana", MONTH: "mês", YEAR: "ano" }[periodicity ?? "MONTH"] ?? "mês";
75
+ if (periodicity === "WEEK") return `A cada ${n * 7} dias`;
76
+ return `A cada ${n} ${unit}${n > 1 && unit !== "mês" ? "s" : ""}`;
77
+ }
78
+
79
+ const NUTRI_LABELS: Record<string, string> = {
80
+ valor_energetico_kcal: "Valor energético", carboidratos_g: "Carboidratos",
81
+ acucares_totais_g: "Açúcares totais", acucares_adicionados_g: "Açúcares adicionados",
82
+ proteinas_g: "Proteínas", gorduras_totais_g: "Gorduras totais",
83
+ gorduras_saturadas_g: "Gorduras saturadas", gorduras_trans_g: "Gorduras trans",
84
+ fibras_alimentares_g: "Fibras alimentares", sodio_mg: "Sódio",
85
+ };
86
+ const fmtNum = (n: number) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(n);
87
+
88
+ // Bandeira (emoji) do país de origem, p/ encaixar no selo "Importado". Usa o país de
89
+ // processamento/importação quando o texto cita "Estados Unidos".
90
+ function originFlag(origin?: string): string | undefined {
91
+ const o = (origin ?? "").toLowerCase();
92
+ if (/estados unidos|united states|unites states|\beua\b|u\.?s\.?a/.test(o)) return "🇺🇸";
93
+ if (/brasil|brazil/.test(o)) return "🇧🇷";
94
+ if (/m[eé]xico/.test(o)) return "🇲🇽";
95
+ if (/[ií]ndia/.test(o)) return "🇮🇳";
96
+ if (/vietn[aã]/.test(o)) return "🇻🇳";
97
+ return undefined;
98
+ }
99
+
100
+ // Tabela nutricional SÓ com dado real do enriquecimento. Sem dado, devolve vazio e a aba nem
101
+ // existe — antes renderizava "Valor energético —, Sódio —..." em loja de qualquer ramo.
102
+ function buildNutriItems(enr: ProductEnrichment | null): { k: string; v: string }[] {
103
+ const nutr = enr?.nutrition?.nutrientes;
104
+ if (nutr && Object.keys(nutr).length) {
105
+ const base = enr?.nutrition?.base ? [{ k: "Base", v: enr.nutrition.base }] : [];
106
+ return [
107
+ ...base,
108
+ ...Object.entries(nutr).map(([key, val]) => {
109
+ const unit = key.endsWith("_kcal") ? "kcal" : key.endsWith("_mg") ? "mg" : key.endsWith("_g") ? "g" : "";
110
+ const label = NUTRI_LABELS[key] || key.replace(/_(g|mg|kcal)$/, "").replace(/_/g, " ");
111
+ return { k: label, v: `${fmtNum(Number(val))} ${unit}`.trim() };
112
+ }),
113
+ ];
114
+ }
115
+ return [];
116
+ }
117
+
118
+ export default async function ProductPage({ params }: { params: Promise<{ productSlug: string }> }) {
119
+ const { productSlug } = await params;
120
+ const [wrapper, shop, catalog] = await Promise.all([
121
+ mockupOr(getProductBySlug(decodeURIComponent(productSlug)), null, "produto/getProductBySlug"),
122
+ mockupOr(getShopData(), null, "produto/getShopData"),
123
+ mockupOr(getCatalog({ first: 100 }), ({ nodes: [] as any[] }), "produto/getCatalog"),
124
+ ]);
125
+ const p = wrapper?.product;
126
+ if (!p || p.isVisible === false) notFound();
127
+
128
+ // Enriquecimento: tudo que não vem da Unbox — composição, medidas, FAQ, reviews, SEO, e tabela
129
+ // nutricional quando o produto for alimento ou suplemento.
130
+ const enr = getEnrichmentForProduct(p);
131
+
132
+ const price = resolveProductPrice(p);
133
+ const policy = shop?.recurringOrdersPolicy;
134
+ const subscribable =
135
+ p.recurrenceAllowed && policy?.enabled && (policy.allowedFrequencies?.length ?? 0) > 0;
136
+
137
+ // Descrição: prioriza a descrição SEO da planilha; senão a da Unbox.
138
+ const shortDescription = enr?.seo.shortDescription || enr?.description || wrapper.shortDescription;
139
+ const descHtml = sanitize(enr?.seo.longDescriptionHtml || p.description);
140
+ const infoHtml = sanitize(p.additionalInformation);
141
+ const rstats = reviewStats(enr);
142
+
143
+ // ----- Variantes para a buy box -----
144
+ const variants: PdpVariant[] = (p.variants ?? []).map((v: any) => {
145
+ const rp = resolveVariantPrice(v);
146
+ const pr = Array.isArray(v?.pricing) ? v.pricing[0] : v?.pricing;
147
+ return {
148
+ id: v._id,
149
+ label: v.title || p.title,
150
+ price: rp.price,
151
+ displayPrice: rp.displayPrice,
152
+ oldPrice: pr?.compareAtPrice?.amount ?? null,
153
+ };
154
+ });
155
+
156
+ // ----- Tamanhos da MESMA família (products.json) -----
157
+ // Cada tamanho é um produto Unbox próprio; agrupamos pelo familyCode da planilha (resolvido por
158
+ // nome, que ignora o tamanho). O peso vem do título do produto.
159
+ const familyCode = enr?.familyCode ?? null;
160
+ const fmtWeight = (w: { value: number; unit: string } | null): string => {
161
+ if (!w) return NAO_INFORMADO;
162
+ const v = Number.isInteger(w.value) ? String(w.value) : String(w.value).replace(".", ",");
163
+ return `${v} ${w.unit}`;
164
+ };
165
+ const allCatalogProducts = (catalog.nodes ?? []).map((n: any) => n.product ?? n).filter((x: any) => x?.slug);
166
+ const sizeOptions: PdpSizeOption[] = familyCode
167
+ ? allCatalogProducts
168
+ .map((cp: any) => {
169
+ const e = getEnrichmentByName(cp.title);
170
+ if (!e || e.familyCode !== familyCode) return null;
171
+ const size = parseSize(cp.title) ?? e.weight;
172
+ const rp = resolveProductPrice(cp);
173
+ return {
174
+ option: {
175
+ slug: cp.slug,
176
+ label: fmtWeight(size),
177
+ displayPrice: rp.displayPrice,
178
+ isSoldOut: !!cp.isSoldOut,
179
+ isCurrent: cp.slug === p.slug,
180
+ } satisfies PdpSizeOption,
181
+ weight: size?.value ?? 0,
182
+ };
183
+ })
184
+ .filter((x: any): x is { option: PdpSizeOption; weight: number } => !!x)
185
+ .sort((a, b) => a.weight - b.weight)
186
+ .map((x) => x.option)
187
+ : [];
188
+
189
+ // ----- Relacionados por CATEGORIA quando não há variação de tamanho -----
190
+ // Produtos DISTINTOS de uma mesma linha (famílias diferentes, não tamanhos), listados 1 por
191
+ // família para navegar entre eles. Quais linhas e com que título: lib/store-config.ts
192
+ // (RELATED_GROUPS). Default vazio — a loja decide.
193
+ const labelPt = (name: string) => name.split("(")[0].trim().replace(/\s+/g, " ") || name.trim();
194
+ const relatedGroup = enr ? RELATED_GROUPS.find((g) => enr.tags?.includes(g.tag)) : undefined;
195
+ const relatedByFamily = new Map<string, { option: PdpRelatedOption; name: string }>();
196
+ if (relatedGroup) {
197
+ for (const cp of allCatalogProducts) {
198
+ const e = getEnrichmentByName(cp.title);
199
+ if (!e || !e.tags?.includes(relatedGroup.tag)) continue;
200
+ if (relatedByFamily.has(e.familyCode)) continue; // 1 produto por família
201
+ const rp = resolveProductPrice(cp);
202
+ relatedByFamily.set(e.familyCode, {
203
+ name: labelPt(e.name),
204
+ option: {
205
+ slug: cp.slug,
206
+ label: labelPt(e.name),
207
+ sublabel: fmtWeight(parseSize(cp.title) ?? e.weight),
208
+ displayPrice: rp.displayPrice,
209
+ imageUrl: cp.imageUrls?.[0] ?? null,
210
+ isSoldOut: !!cp.isSoldOut,
211
+ isCurrent: cp.slug === p.slug,
212
+ },
213
+ });
214
+ }
215
+ }
216
+ const relatedOptions: PdpRelatedOption[] = [...relatedByFamily.values()]
217
+ .map((x) => x.option)
218
+ .sort((a, b) => a.label.localeCompare(b.label, "pt-BR"));
219
+ const relatedTitle = relatedGroup?.title;
220
+
221
+ // ----- Selos (benefícios) a partir dos atributos do enriquecimento — só positivos, máx. 4 -----
222
+ const benefits: PdpBenefit[] | undefined = enr
223
+ ? (() => {
224
+ const b: PdpBenefit[] = [];
225
+ if (enr.containsGluten === false) b.push({ icon: "no-gluten", label: "Sem glúten" });
226
+ if (enr.vegan === true) b.push({ icon: "vegan", label: "Vegano" });
227
+ // "Sem MSG" é afirmação de ALIMENTO. Só entra quando o produto tem tabela nutricional (é
228
+ // alimento/suplemento) — antes, qualquer composição sem a palavra "glutamato" ganhava o
229
+ // selo, inclusive INCI de cosmético e lista de materiais de roupa.
230
+ if (enr.nutrition && enr.ingredients && !/glutamato|\bmsg\b|realçador|realcador/i.test(enr.ingredients)) b.push({ icon: "no-msg", label: "Sem MSG" });
231
+ if (enr.origin) b.push({ icon: "imported", label: "Importado", flag: originFlag(enr.origin) });
232
+ return b.slice(0, 4);
233
+ })()
234
+ : undefined;
235
+
236
+ const subscription = subscribable
237
+ ? {
238
+ percentOff: policy.pricingPolicy?.type === "PERCENTAGE_OFF" ? policy.pricingPolicy.value : null,
239
+ frequencies: (policy.allowedFrequencies ?? []).map((f: any) => ({
240
+ id: f._id,
241
+ title: f.title,
242
+ note: freqNote(f.periodicity, f.interval),
243
+ })),
244
+ }
245
+ : null;
246
+
247
+ // ----- Desconto exibido nos badges da galeria -----
248
+ const v0 = variants[0];
249
+ const discountPct =
250
+ v0?.price != null && v0.oldPrice && v0.oldPrice > v0.price
251
+ ? Math.round((1 - v0.price / v0.oldPrice) * 100)
252
+ : null;
253
+
254
+ // ----- Ficha técnica: SÓ as linhas que têm dado -----
255
+ // Sem enriquecimento não há ficha, e a aba não existe. Antes a foundation exibia "Glúten —",
256
+ // "Vegano —", "Ingredientes —" em loja de qualquer ramo: era o esqueleto de um produto de
257
+ // alimentação aparecendo como característica de roupa, cosmético ou eletrônico.
258
+ const specItems: { k: string; v: string }[] = [];
259
+ if (enr) {
260
+ if (enr.weight) specItems.push({ k: "Peso", v: `${String(enr.weight.value).replace(".", ",")} ${enr.weight.unit}` });
261
+ if (enr.origin) specItems.push({ k: "Origem", v: enr.origin });
262
+ if (enr.ingredients) specItems.push({ k: "Composição", v: enr.ingredients });
263
+ if (enr.containsGluten != null) specItems.push({ k: "Glúten", v: enr.containsGluten ? "Contém" : "Não contém" });
264
+ if (enr.vegan != null) specItems.push({ k: "Vegano", v: enr.vegan ? "Sim" : "Não" });
265
+ if (enr.allergens?.mayContain && !/não especificado/i.test(enr.allergens.mayContain)) specItems.push({ k: "Pode conter", v: enr.allergens.mayContain });
266
+ if (enr.ean) specItems.push({ k: "EAN", v: enr.ean });
267
+ }
268
+ const nutriItems = buildNutriItems(enr);
269
+
270
+ // FAQ do PRODUTO: as perguntas do enriquecimento (dado). As perguntas MODELO do molde (o prazo de
271
+ // arrependimento do CDC e a regra de frete grátis, se configurada) moram em faq-modelo.ts: a FaqList
272
+ // as renderiza como lista editável, e o FAQPage abaixo as lê do documento publicado.
273
+ const faq = enr?.faq ?? [];
274
+
275
+ // ----- Catálogo relacionado (produtos reais) -----
276
+ const recItems: CatalogItem[] = (catalog.nodes ?? [])
277
+ .map((n: any) => n.product ?? n)
278
+ .filter((rp: any) => rp?.slug && rp.slug !== p.slug)
279
+ .slice(0, 9)
280
+ .map((rp: any): CatalogItem => {
281
+ const rprice = resolveProductPrice(rp);
282
+ const rv0 = rp.variants?.[0];
283
+ return {
284
+ slug: rp.slug,
285
+ title: rp.title,
286
+ displayPrice: rprice.displayPrice,
287
+ imageUrl: rp.imageUrls?.[0] ?? null,
288
+ weight: rv0?.title ?? null,
289
+ productId: rp.productId,
290
+ variantId: rv0?._id ?? null,
291
+ price: rv0?.pricing?.[0]?.price ?? null,
292
+ badge: rp.isSoldOut
293
+ ? { label: "ESGOTADO", bg: "var(--store-surface-2)", fg: "var(--store-muted)" }
294
+ : rprice.compareAt
295
+ ? { label: "OFERTA", bg: "var(--store-sale-soft)", fg: "var(--store-sale)" }
296
+ : rp.isLowQuantity
297
+ ? { label: "ÚLTIMAS", bg: "var(--store-cta-soft)", fg: "var(--store-cta-fg)" }
298
+ : null,
299
+ };
300
+ });
301
+ const catalogItems: CatalogItem[] = recItems.slice(0, 5);
302
+
303
+ // FAQPage a partir da MESMA lista que o accordion renderiza: se o bloco some da tela, some
304
+ // do dado estruturado junto. Pergunta que a loja não responde não vira schema. Com o editor, o que
305
+ // o accordion renderiza depende do publicado (seção oculta, pergunta reordenada, reescrita ou
306
+ // duplicada), então a lista é lida do documento, do mesmo jeito que os primitivos a leem.
307
+ const faqPublicado = faqNaTela(await getPublishedContent(), faq);
308
+ const faqJsonLd = faqPublicado.length > 0 ? {
309
+ "@context": "https://schema.org",
310
+ "@type": "FAQPage",
311
+ mainEntity: faqPublicado.map((f) => ({
312
+ "@type": "Question",
313
+ name: f.question,
314
+ acceptedAnswer: { "@type": "Answer", text: f.answer },
315
+ })),
316
+ } : null;
317
+
318
+ const jsonLd = {
319
+ "@context": "https://schema.org",
320
+ "@type": "Product",
321
+ name: p.title,
322
+ image: p.imageUrls ?? [],
323
+ description: (p.metaDescription || wrapper.shortDescription || p.title) as string,
324
+ sku: p.sku ?? undefined,
325
+ // aggregateRating SÓ com avaliações reais (rstats): nota fabricada em dado ESTRUTURADO é o
326
+ // pior lugar possível pra mentir — vai direto pro Google (rich results) em nome da loja.
327
+ ...(rstats ? { aggregateRating: { "@type": "AggregateRating", ratingValue: String(rstats.average), reviewCount: String(rstats.count) } } : {}),
328
+ offers: {
329
+ "@type": "Offer",
330
+ price: price.price ?? undefined,
331
+ priceCurrency: "BRL",
332
+ availability: p.isSoldOut ? "https://schema.org/OutOfStock" : "https://schema.org/InStock",
333
+ url: wrapper.publishedUrl || `${siteUrl}/produto/${productSlug}`,
334
+ },
335
+ };
336
+
337
+ return (
338
+ <>
339
+ {/* dataLayerReady: o gatilho de tipo de página do container central da Unbox (não renderiza nada) */}
340
+ <DataLayerReady pageType="product" products={[{ id: p.productId ?? p._id, name: p.title, price: price.price ?? undefined }]} />
341
+ <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: ldJson(jsonLd) }} />
342
+ {faqJsonLd && <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: ldJson(faqJsonLd) }} />}
343
+ <PdpView
344
+ produto={{
345
+ id: p.productId,
346
+ titulo: p.title,
347
+ imagens: p.imageUrls ?? [],
348
+ videos: p.videoUrls ?? [],
349
+ descontoPct: discountPct,
350
+ descricaoCurta: shortDescription,
351
+ esgotado: !!p.isSoldOut,
352
+ minQty: p.minOrderQuantity ?? 1,
353
+ maxQty: p.maxOrderQuantity ?? null,
354
+ }}
355
+ compra={{
356
+ variants,
357
+ benefits,
358
+ sizeOptions,
359
+ relatedTitle,
360
+ relatedOptions,
361
+ subscription,
362
+ ratingAverage: rstats ? Number(rstats.average) : undefined,
363
+ ratingCount: rstats ? Number(rstats.count) : undefined,
364
+ }}
365
+ detalhes={{ descHtml, infoHtml, specItems, nutriItems, nutriBase: enr?.nutrition?.base, usageSteps: enr?.usage }}
366
+ avaliacoes={rstats && enr?.reviews?.length ? { ratingCount: Number(rstats.count), average: Number(rstats.average), reviews: enr.reviews } : null}
367
+ faq={faq}
368
+ catalogo={catalogItems}
369
+ />
370
+ </>
371
+ );
372
+ }
@@ -0,0 +1,16 @@
1
+ import { CatalogGridSkeleton } from "@/components/catalog/grid-skeleton";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div>
6
+ <div className="mb-5 flex flex-wrap items-center justify-between gap-3">
7
+ <div className="space-y-2">
8
+ <div className="h-7 w-48 animate-pulse rounded bg-muted" />
9
+ <div className="h-4 w-24 animate-pulse rounded bg-muted" />
10
+ </div>
11
+ <div className="h-9 w-40 animate-pulse rounded-md bg-muted" />
12
+ </div>
13
+ <CatalogGridSkeleton count={8} />
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,33 @@
1
+ import type { Metadata } from "next";
2
+ import { getCatalog, getTopTags } from "@/lib/queries";
3
+ import { buildTagMap, buildCategories, mapCatalogItems } from "@/lib/catalog-map";
4
+ import { resolveCombos } from "@/lib/enrichment/combos";
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
+ export const metadata: Metadata = { title: "Catálogo", alternates: { canonical: "/produtos" } };
11
+
12
+ export default async function ProdutosPage() {
13
+ // Catálogo completo (a loja tem dezenas de produtos) — filtragem/ordenação/paginação é client-side.
14
+ const [catalog, tags] = await Promise.all([
15
+ mockupOr(getCatalog({ first: 100 }), { nodes: [] as any[] }, "produtos/getCatalog"),
16
+ mockupOr(getTopTags(), [], "produtos/getTopTags"),
17
+ ]);
18
+ const tagMap = buildTagMap(tags as any[]);
19
+ const bundles = resolveCombos((catalog.nodes ?? []).map((n: any) => n.product ?? n));
20
+ const itens = mapCatalogItems(catalog.nodes ?? [], tagMap);
21
+ // EDITOR: /produtos é a dona do container "catalogo" (layout padrão: manda na ordem, oculta e copia
22
+ // seções); /categoria/[tagSlug] reaproveita a mesma copy com layout={false}.
23
+ return (
24
+ <>
25
+ <DataLayerReady pageType="category" products={itens.slice(0, 12).map((i) => ({ id: i.productId, name: i.title, price: i.price }))} />
26
+ <CatalogClient
27
+ items={itens}
28
+ categories={buildCategories(tags as any[])}
29
+ bundles={bundles}
30
+ />
31
+ </>
32
+ );
33
+ }
@@ -0,0 +1,104 @@
1
+ import type { Metadata } from "next";
2
+ import Link from "next/link";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Termos de Uso",
6
+ robots: { index: true, follow: true },
7
+ alternates: { canonical: "/termos" },
8
+ };
9
+
10
+ // TODO [JURÍDICO]: revisar e completar antes do lançamento.
11
+ // Substituir [NOME DA LOJA], [CNPJ], [ENDEREÇO], [EMAIL ATENDIMENTO] pelos dados reais.
12
+
13
+ export default function TermosPage() {
14
+ return (
15
+ <article className="richtext mx-auto max-w-2xl py-8 px-4">
16
+ <h1>Termos de Uso</h1>
17
+ <p className="text-sm text-muted-foreground">Última atualização: preencher data</p>
18
+
19
+ <h2>1. Aceitação dos termos</h2>
20
+ <p>
21
+ Ao acessar e utilizar este site, você concorda com estes Termos de Uso e com nossa{" "}
22
+ <Link href="/privacidade" className="text-primary underline">Política de Privacidade</Link>.
23
+ Se não concordar, não utilize o site.
24
+ </p>
25
+
26
+ <h2>2. Sobre a loja</h2>
27
+ <p>
28
+ <strong>[NOME DA LOJA]</strong>, inscrita no CNPJ nº <strong>[CNPJ]</strong>,
29
+ com sede em <strong>[ENDEREÇO COMPLETO]</strong>, opera esta loja com tecnologia
30
+ headless da plataforma Unbox (unbox.com.br).
31
+ </p>
32
+
33
+ <h2>3. Preços e disponibilidade</h2>
34
+ <p>
35
+ Os preços, estoque e condições são atualizados em tempo real e podem mudar sem aviso prévio.
36
+ O preço válido é o exibido no momento da finalização do pedido.
37
+ </p>
38
+
39
+ <h2>4. Pagamentos</h2>
40
+ <ul>
41
+ <li><strong>Pix:</strong> pedidos são confirmados após a compensação do pagamento (geralmente instantânea).</li>
42
+ <li><strong>Cartão de crédito:</strong> a cobrança é realizada no momento da compra. Parcelamento conforme condições exibidas no checkout.</li>
43
+ <li>Os dados do cartão são processados diretamente pela <strong>UnboxPay</strong>, e não armazenamos dados de pagamento.</li>
44
+ </ul>
45
+
46
+ <h2>5. Frete e entrega</h2>
47
+ <p>
48
+ Os prazos e valores de frete são calculados no checkout com base no CEP informado.
49
+ O prazo começa a contar após a confirmação do pagamento. Atrasos causados por
50
+ eventos externos (força maior, greves, etc.) estão fora de nossa responsabilidade.
51
+ </p>
52
+
53
+ <h2>6. Assinatura recorrente</h2>
54
+ <ul>
55
+ <li>Produtos assináveis são cobrados automaticamente na frequência escolhida (mensal, bimestral etc.).</li>
56
+ <li>Você pode pausar, pular ciclos ou cancelar a qualquer momento na{" "}
57
+ <Link href="/conta/assinaturas" className="text-primary underline">área da conta</Link>.
58
+ </li>
59
+ <li>Assinantes têm acesso a descontos exclusivos e, em algumas campanhas, a lançamentos antecipados.</li>
60
+ <li>O cancelamento encerra as cobranças futuras; pedidos já processados seguem normalmente.</li>
61
+ </ul>
62
+
63
+ <h2>7. Trocas e devoluções</h2>
64
+ <p>
65
+ Consulte nossa{" "}
66
+ <Link href="/devolucoes" className="text-primary underline">Política de Trocas e Devoluções</Link>{" "}
67
+ para informações sobre direito de arrependimento (CDC, art. 49) e como solicitar reembolso.
68
+ </p>
69
+
70
+ <h2>8. Propriedade intelectual</h2>
71
+ <p>
72
+ Todo o conteúdo deste site (textos, imagens, logotipos, layouts) é propriedade de
73
+ <strong> [NOME DA LOJA]</strong> ou de seus fornecedores e está protegido pelas leis de
74
+ direitos autorais. É proibida a reprodução sem autorização prévia por escrito.
75
+ </p>
76
+
77
+ <h2>9. Limitação de responsabilidade</h2>
78
+ <p>
79
+ Não nos responsabilizamos por danos indiretos, incidentais ou consequentes decorrentes
80
+ do uso do site ou dos produtos, exceto nos casos previstos no Código de Defesa do
81
+ Consumidor (Lei 8.078/1990).
82
+ </p>
83
+
84
+ <h2>10. Privacidade e LGPD</h2>
85
+ <p>
86
+ O tratamento de dados pessoais segue nossa{" "}
87
+ <Link href="/privacidade" className="text-primary underline">Política de Privacidade</Link>,
88
+ em conformidade com a Lei Geral de Proteção de Dados (Lei 13.709/2018).
89
+ </p>
90
+
91
+ <h2>11. Foro</h2>
92
+ <p>
93
+ Fica eleito o foro da comarca de <strong>[CIDADE/ESTADO]</strong> para dirimir
94
+ eventuais litígios decorrentes destes Termos, com renúncia a qualquer outro,
95
+ por mais privilegiado que seja.
96
+ </p>
97
+
98
+ <h2>12. Contato</h2>
99
+ <p>
100
+ Dúvidas sobre estes termos: <strong>[EMAIL ATENDIMENTO]</strong>
101
+ </p>
102
+ </article>
103
+ );
104
+ }
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ // Porta do preview — prévia privada com captura de lead. Uma tela própria em vez do
4
+ // Basic Auth do navegador: o link vai pro dono da marca, e a caixa cinza do sistema
5
+ // pedindo usuário e senha passa a impressão de site quebrado.
6
+ //
7
+ // Fica FORA do route group (loja) de propósito: nasce sem header/rodapé/nav da loja.
8
+ import * as React from "react";
9
+ import { useSearchParams } from "next/navigation";
10
+ import { ArrowRight, LockSimple } from "@phosphor-icons/react/dist/ssr";
11
+ import { maskPhone } from "@/lib/format";
12
+
13
+ export default function AcessoPage() {
14
+ return (
15
+ // Suspense: useSearchParams obriga, senão a rota inteira vira dinâmica.
16
+ <React.Suspense fallback={null}>
17
+ <Porta />
18
+ </React.Suspense>
19
+ );
20
+ }
21
+
22
+ function Porta() {
23
+ const params = useSearchParams();
24
+ const [nome, setNome] = React.useState("");
25
+ const [marca, setMarca] = React.useState("");
26
+ const [whatsapp, setWhatsapp] = React.useState("");
27
+ const [email, setEmail] = React.useState("");
28
+ const [erro, setErro] = React.useState(false);
29
+ const [enviando, setEnviando] = React.useState(false);
30
+ const valido = nome.trim().length > 1 && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim());
31
+
32
+ async function entrar(e: React.FormEvent) {
33
+ e.preventDefault();
34
+ if (enviando || !valido) return;
35
+ setEnviando(true);
36
+ setErro(false);
37
+ try {
38
+ const r = await fetch("/api/acesso", {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ nome, marca, whatsapp, email }),
42
+ });
43
+ if (!r.ok) {
44
+ setErro(true);
45
+ setEnviando(false);
46
+ return;
47
+ }
48
+ // Só caminho interno começando com "/" — senão ?de=https://outro-site vira
49
+ // redirecionamento aberto, cortesia da nossa própria tela de acesso.
50
+ const de = params.get("de") ?? "/";
51
+ const destino = de.startsWith("/") && !de.startsWith("//") ? de : "/";
52
+ // NAVEGAÇÃO DE DOCUMENTO INTEIRO, não router.replace(): o App Router guarda em
53
+ // cache o payload RSC do destino — que é o REDIRECT pra /acesso, gerado antes do
54
+ // cookie existir. router.replace() voltava pra própria porta (bug real em produção).
55
+ window.location.replace(destino);
56
+ } catch {
57
+ setErro(true);
58
+ setEnviando(false);
59
+ }
60
+ }
61
+
62
+ const campos = [
63
+ { id: "nome", rotulo: "Nome completo", valor: nome, muda: setNome, tipo: "text", ph: "Seu nome", auto: "name", foco: true },
64
+ { id: "marca", rotulo: "Nome da sua marca", valor: marca, muda: setMarca, tipo: "text", ph: "Sua marca", auto: "organization", foco: false },
65
+ { id: "whatsapp", rotulo: "WhatsApp", valor: whatsapp, muda: (v: string) => setWhatsapp(maskPhone(v)), tipo: "tel", ph: "(11) 99999-8888", auto: "tel", foco: false },
66
+ { id: "email", rotulo: "E-mail", valor: email, muda: setEmail, tipo: "email", ph: "voce@empresa.com.br", auto: "email", foco: false },
67
+ ];
68
+
69
+ return (
70
+ <div className="store-layout flex min-h-[100svh] items-center justify-center bg-[var(--store-bg)] px-5 py-16">
71
+ <div className="w-full max-w-[430px] text-center">
72
+ {/* eslint-disable-next-line @next/next/no-img-element -- logo do chrome: SVG de poucos KB. O next/image marcaria lazy num elemento que aparece em toda página (o preload scanner perde o recurso) e o reencode come o traço fino do lettering. Otimizar poucos KB não paga essas duas contas. */}
73
+ <img src="/brand/logo.svg" alt="Logo da loja" className="mx-auto h-14 w-auto" />
74
+
75
+ <div className="mt-8 rounded-[26px] border border-[var(--store-line)] bg-[var(--store-surface)] p-7 sm:p-9">
76
+ <span className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-[var(--store-primary,#18181B)]">
77
+ <LockSimple weight="fill" className="text-[26px] text-white" />
78
+ </span>
79
+
80
+ <h1 className="font-display mt-5 text-[1.6rem] font-extrabold leading-tight text-[var(--store-ink)]">
81
+ Prévia privada
82
+ </h1>
83
+ <p className="mt-2.5 text-[.98rem] leading-[1.55] text-[var(--store-muted)]">
84
+ Esta é uma versão em construção da loja, ainda não publicada. Preencha para visualizar.
85
+ </p>
86
+
87
+ <form onSubmit={entrar} className="mt-7 space-y-4 text-left">
88
+ {campos.map((c) => (
89
+ <div key={c.id}>
90
+ <label htmlFor={c.id} className="mb-1.5 block text-[0.85rem] font-extrabold text-[var(--store-ink)]">
91
+ {c.rotulo}
92
+ </label>
93
+ <input
94
+ id={c.id}
95
+ type={c.tipo}
96
+ value={c.valor}
97
+ autoFocus={c.foco}
98
+ autoComplete={c.auto}
99
+ inputMode={c.id === "whatsapp" ? "tel" : undefined}
100
+ onChange={(e) => { c.muda(e.target.value); setErro(false); }}
101
+ placeholder={c.ph}
102
+ className="h-[50px] w-full rounded-full border border-[var(--store-line-2)] bg-[var(--store-surface)] px-5 text-[0.98rem] text-[var(--store-ink)] outline-none transition-colors focus:border-[var(--store-primary,#18181B)]"
103
+ />
104
+ </div>
105
+ ))}
106
+
107
+ {erro && (
108
+ <p role="alert" className="text-[.9rem] font-semibold text-[var(--store-primary,#18181B)]">
109
+ Confere o nome e o e-mail e tenta de novo.
110
+ </p>
111
+ )}
112
+
113
+ <button
114
+ type="submit"
115
+ disabled={enviando || !valido}
116
+ className="mt-1 flex h-14 w-full cursor-pointer items-center justify-center gap-2.5 rounded-full bg-[var(--store-primary,#18181B)] text-[16px] font-bold text-white transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
117
+ >
118
+ {enviando ? "Entrando..." : <>Ver a loja <ArrowRight weight="bold" /></>}
119
+ </button>
120
+ </form>
121
+ </div>
122
+
123
+ <p className="mt-6 text-[.85rem] text-[var(--store-muted)]">
124
+ Usamos seus dados apenas para falar com você sobre esta prévia.
125
+ </p>
126
+ </div>
127
+ </div>
128
+ );
129
+ }