@ocahub/kit
v0.3.0
Published
Módulo Nuxt da plataforma: páginas e componentes da loja (prateleira e detalhe do produto) ligados aos endpoints públicos do sales-api
Readme
rhfactor-kit
Módulo Nuxt 4.5 da plataforma. Com uma linha de configuração (o id da loja), o site do cliente ganha as páginas da loja já funcionando, e cada pedaço delas pode ser trocado.
Stack: Nuxt 4.5 · @nuxt/module-builder · TypeScript · CSS puro (sem framework)
O que o módulo entrega
| | |
|---|---|
| Páginas injetadas | / prateleira (todos os produtos ativos na loja, 24 por página, ?page=) · /produto/{slug} detalhe · /carrinho · conta do cliente: /login, /cadastro, /cadastro/dados, /termos, /minha-conta, /confirmar-email · checkout e pedidos (00017): /carrinho/checkout (identificação), /carrinho/checkout/frete (entrega), /compras/{codigo}/pagamento, /compras/{codigo}, /minha-conta/pedidos, /minha-conta/enderecos · compra coletiva (00018): /rodadas/{rodada} (hotsite) e /rodadas/{rodada}/{produto} |
| Componentes | RhProductGrid → RhProductCard · RhProductGallery (troca e zoom) · RhVariantSelector · RhProductProperties · RhProductDescription · RhAddToCartButton · compra coletiva: RhCompraColetiva (destaque da home) → RhRoundCard, RhOfferCard, RhReserveButton, RhLotProgress (caixa compartilhada) · RhPrice · RhImage · RhPagination · RhStoreHeader · RhStoreFooter · RhCartButton · RhAccountButton |
| Composables | useRhStore() · useRhProducts(page) · useRhProduct(slug) · useRhFormat() · useRhLinks() · useRhCart() (store Pinia do carrinho) · useRhHomeRounds(), useRhRound(slug), useRhRoundProduct(slug, produto) · useRhAccount() (sessão e cadastro do cliente) |
| Rotas de servidor | /api/rhfactor/stores/{id}, …/products, …/products/{slug}, …/cart, …/account/*: o navegador nunca fala com o sales-api, o IAM ou o contract-management |
| Dados no build | nome, descrição, logotipo e formato das imagens da loja entram no app.config |
RhShelfPage RhProductPage
└─ RhProductGrid ├─ RhProductGallery (miniaturas, zoom no mouse, tela cheia, arrastar no celular)
└─ RhProductCard ├─ RhPrice (preço do SKU escolhido)
├─ RhImage ├─ RhVariantSelector (cor, tamanho, peso...; indisponível riscado)
└─ RhPrice ├─ RhAddToCartButton (quantidade + hook do carrinho)
└─ RhPagination ├─ RhProductDescription
└─ RhProductPropertiesUso
// nuxt.config.ts do site
export default defineNuxtConfig({
modules: ['@ocahub/kit'],
rhfactor: {
storeId: process.env.RHFACTOR_STORE_ID, // painel > Minha loja
apiBase: process.env.RHFACTOR_API_BASE // sales-api visto pelo servidor
}
})| Opção | Padrão | |
|---|---|---|
| storeId | RHFACTOR_STORE_ID | id da loja; sem ele, as páginas respondem 503 "loja não configurada" |
| apiBase | RHFACTOR_API_BASE ou http://localhost:8085 | só servidor; em execução, NUXT_RHFACTOR_API_BASE troca sem novo build |
| iamBase | RHFACTOR_IAM_BASE ou http://localhost:8080 | IAM (cadastro e login do cliente); em execução, NUXT_RHFACTOR_IAM_BASE |
| contractsBase | RHFACTOR_CONTRACTS_BASE ou http://localhost:8082 | contract-management (termos da loja); em execução, NUXT_RHFACTOR_CONTRACTS_BASE |
| paymentsBase | RHFACTOR_PAYMENTS_BASE ou http://localhost:8089 | payments-service (meios de pagamento da loja no checkout); em execução, NUXT_RHFACTOR_PAYMENTS_BASE |
| cacheSeconds | 60 | cache stale-while-revalidate das rotas /api/rhfactor/**; 0 desliga |
| pages | true | false desliga todas; { shelf: false }, { product: '/p' }, { login: '/entrar' }... |
| css | true | false tira o tema padrão (estilize as classes .rh-* do zero) |
| prefetchStore | true | busca os dados da loja no build |
Personalizar, do mais leve ao mais profundo
- Tokens — sobrescreva no CSS do site:
--rh-color-accent,--rh-font,--rh-radius,--rh-grid-min... (lista emsrc/runtime/assets/rhfactor.css). - Dados da loja só no site —
app.config.ts:rhfactor: { store: { logoUrl: '…' } }. - Um componente — crie no site um componente com o mesmo nome (ex.:
app/components/RhProductCard.vue); o do site tem prioridade. - Um pedaço da grade —
<RhProductGrid :products><template #item="{ product }">…. - A página inteira — crie
app/pages/index.vueouapp/pages/produto/[slug].vue: a página do site sempre vence a do kit. Monte com os composables e componentes.
Carrinho
- Página
/carrinho(RhCartPage): itens com o preço da loja agora, a quantidade (no box, de caixa em caixa), remover e o resumo. "Finalizar compra" leva ao/carrinho/checkout, que pede conta. RhCartButton: a sacola com o contador, para o cabeçalho.RhAddToCartButton: grava no carrinho e soma ao que já está lá. Quando a loja recusa (estoque), mostra a mensagem dela.- Estado na tela: um store Pinia (
useRhCart(), instalado pelo kit). O id e o segredo do carrinho não ficam nele: ficam num cookiehttpOnlypor loja (rh_cart_*, 30 dias). - Quem fala com o sales-api: as rotas
/api/rhfactor/stores/{id}/cart(GET, e PUT/DELETE decart/items/{sku}), sem cache. O carrinho só é criado no primeiro item. - Hook
rhfactor:cart:add: continua disparando a cada adição, para o site avisar.
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.hook('rhfactor:cart:add', item => { /* ... */ })
})Checkout e pedidos (00017)
/carrinho ─▶ /carrinho/checkout ─▶ /carrinho/checkout/frete ─▶ /compras/{codigo}/pagamento ─▶ payments.ocahub.com.br/{transação} ─▶ /compras/{codigo}
identificação endereço + entrega escolhe o meio paga no gateway acompanha- Entrega (
RhCheckoutDeliveryPage): endereço do caderno (IAM) ou novo — o CEP preenche o resto (ViaCEP, do navegador). Com itens de prazos de preparo diferentes, o cliente escolhe tudo junto ou separado (um pacote por prazo); cada pacote tem as opções cotadas (transportadoras + retirada na loja). O valor do frete vem da cotação guardada no sales-api, nunca do navegador. - Fechar pedido congela itens, preços e entrega; o carrinho vira pedido.
- Pagamento (
RhPurchasePaymentPage): pede e-mail confirmado (409EMAIL_NOT_VERIFIED). A volta do pagamento é montada pelo servidor do site. - Compra (
RhPurchasePage): andamento, entregas com rastreio, pagamentos, histórico; "pagar de novo" e "cancelar" enquanto não pago. Voltando do pagamento, confere a situação por alguns instantes. - Rotas de servidor:
/api/rhfactor/stores/{id}/checkout,…/checkout/freight-quotes,…/checkout/orders,…/purchases[/{codigo}[/payments|/cancel]],…/payment-methods,…/account/addresses[/{id}]. Todas pedem a sessão do cliente. - Componentes:
RhCheckoutSteps,RhAddressForm,RhOrderStatusBadge,RhOrderProgress.
Conta do cliente
O cliente é um usuário da plataforma com o papel CLIENT na tenant da loja. A
sessão do IAM fica em cookies httpOnly por loja (rh_at_*, rh_rt_*); o
script da página nunca vê os tokens. Depois de todo login, a mesma pergunta:
/minha-conta ou /carrinho/checkout ──(sem sessão)──▶ /login?volta=… ──▶ falta nome e CPF? /cadastro/dados
falta aceitar os termos da loja? /termos
nada? volta para onde ia- Cadastro (
/cadastro→/cadastro/dados→/termos): e-mail e senha (mínimo de 12 caracteres), depois nome e CPF, depois os termos de uso da loja. O e-mail de boas-vindas leva o link/confirmar-email; confirmar não bloqueia nada agora (o pagamento vai pedir). - CPF que já tem conta: o cadastro recém-começado é descartado e a tela mostra o e-mail daquela conta, mascarado, para a pessoa entrar com ela.
- E-mail que já tem conta (cliente de outra loja, ou dona da loja): entra com ela; o login pelo site a associa à tenant como cliente.
- Termos de uso: o contrato que a loja escolheu no painel. Versão nova publicada, o cliente aceita de novo no próximo login.
- Carrinho: sem pendências, o carrinho do cookie passa a ser do cliente (ou é juntado ao que ele já tinha nesta loja). Daí em diante quem abre o carrinho é a sessão; ao sair da conta, o carrinho sai do navegador.
- Páginas que pedem conta: o middleware
rh-account(ligado em/minha-conta, o checkout e as compras). Numa página própria do site:definePageMeta({ middleware: 'rh-account' }).
Compra coletiva (00018)
A compra coletiva não é uma forma de vender o produto na loja: é uma feature da loja (o plano libera, a loja liga em Loja › Features no painel) que acontece em rodadas — páginas temáticas com início e fim ("Tommy Hilfiger esta semana"). Cada produto da rodada (sempre sem variação) tem o preço da rodada, a reserva por unidade e o tamanho da caixa compartilhada.
<RhCompraColetiva />lista as rodadas abertas com a capa (8:3). A prateleira padrão já o põe no topo da primeira página. Ele aparece só com a feature ligada e "exibir rodadas na home" marcada;alwaysignora essa opção./rodadas/{rodada}: hotsite com a capa, o tema, até quando vai e os produtos, cada um comRhLotProgress(caixas fechadas, "7 de 10", "faltam 3")./rodadas/{rodada}/{produto}: galeria, preço da rodada, reserva e oRhReserveButton. A reserva entra no carrinho como outra linha (comofferId) e vale o preço da reserva; no checkout o cliente paga as reservas e o frete (calculado sobre o produto final), num pedido de reserva próprio. O restante é cobrado no fechamento da rodada.
Dados
Vêm dos endpoints públicos do sales-api (/public/v1/stores/{id}, ver README do
sales-api). Aparece o produto ativo na loja, com status ativo e ao menos um
SKU ativo; o preço é o da loja. Sem saldo de estoque: o site só sabe se o
SKU está disponível, se é sob encomenda e o prazo. Loja de tenant sem plano de
vendas responde 404, e o site sai do ar com a assinatura.
As imagens vêm no formato da loja (quadrado 1:1 ou retrato 3:4): thumb (300)
na grade, full (1200) no detalhe, original no zoom de tela cheia.
Desenvolvendo o kit: a loja teste
O playground é uma loja teste na stack de desenvolvimento (dev/, com
./scripts/stack.sh dev no ar):
npm install
npm run dev:prepare
npm run loja-teste # cria/atualiza a "Loja Teste" e grava RHFACTOR_STORE_ID em playground/.env
npm run dev # http://localhost:3012O loja-teste usa uma tenant sintética (loja-teste, só no sales-api e no
files-service, sem cadastro no core-api) e cria pela API de verdade:
- 15 produtos: 6 cafés em grade peso × moagem, 3 microlotes (sem variação, só na rodada) e 6 utensílios, alguns com cor;
- estoque com pronta entrega, esgotado e sob encomenda, e um produto sem foto;
- compra coletiva ligada (com destaque na home) e a "Rodada Chapada Diamantina",
aberta por 7 dias a cada execução: caixa de 10 com 57 reservados (5 fechadas,
faltam 3), caixa de 6 com 2 e caixa de 12 com 17. As reservas de exemplo vão
direto no contador das ofertas (
round_offers.reserved) do banco de dev; - 1 a 3 fotos PNG geradas na hora, processadas pelo files-service.
O plano da tenant entra pelo evento de assinatura no Kafka do container, e o token é assinado com a chave de desenvolvimento do IAM: só funciona na stack local. O script pode rodar de novo; ele reaproveita o que já existe.
Loja de uma tenant de verdade (a que o store-whitelabel mostra na porta
3003): npm run loja-real monta o mesmo catálogo entrando com o login da dona
(LOJA_EMAIL / LOJA_SENHA; padrão [email protected], a tenant "Café
Boa Vista" criada antes com npm run tenant:loja no release-check). O id da loja
vai para store-whitelabel/.env.
Os dois scripts gravam também o id no dev/.env (KIT_PLAYGROUND_STORE_ID e
WHITELABEL_STORE_ID), de onde o docker compose sobe os sites:
docker compose --profile loja up -d --build kit-playground store-whitelabel
# http://localhost:3002 playground do kit (Dockerfile.playground, compilado do src/)
# http://localhost:3003 store-whitelabelSem a stack, use o sales-api simulado (qualquer id de loja responde):
npm run mock # porta 8185
RHFACTOR_API_BASE=http://localhost:8185 RHFACTOR_STORE_ID=qualquer npm run devEle tem 26 produtos, com os mesmos casos. FORMAT=PORTRAIT simula uma loja com fotos 3:4.
npm run build && npm run typecheckO site consome o dist/ (não o src/): depois de mudar o kit, rode
npm run kit:sync no store-whitelabel.
