@rocketsearch/nuxt-uappi 0.1.0 → 0.2.1
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/module.js +3 -2
- package/package.json +1 -1
- package/runtime/components/RocketSearch.vue +1 -1
- package/runtime/listing.js +189 -0
- package/runtime/plugin.js +6 -0
- package/runtime/rocket.js +17 -0
package/module.js
CHANGED
|
@@ -77,8 +77,9 @@ module.exports = function rocketSearchUappi (moduleOptions) {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// O pacote chega fora de node_modules via link em dev e dentro dele em produção:
|
|
80
|
-
// nos dois casos o Babel do tema precisa transpilar.
|
|
81
|
-
|
|
80
|
+
// nos dois casos o Babel do tema precisa transpilar. O nome cobre o import direto do
|
|
81
|
+
// tema (runtime/listing.js): sem ele o bundle do SSR faz require() de ES module.
|
|
82
|
+
this.options.build.transpile.push(__dirname, module.exports.meta.name);
|
|
82
83
|
|
|
83
84
|
// Conexão fria + preflight CORS era o grosso da latência do storefront, não a busca.
|
|
84
85
|
this.options.head.link = this.options.head.link || [];
|
package/package.json
CHANGED
|
@@ -174,9 +174,9 @@
|
|
|
174
174
|
import RocketSearchFacets from './search/RocketSearchFacets.vue';
|
|
175
175
|
import RocketSearchPagination from './search/RocketSearchPagination.vue';
|
|
176
176
|
import { fetchUappiProducts, productImage, productPath, productPrice, formatCurrency } from '../uappi.js';
|
|
177
|
+
import { TRACKING_PARAMS } from '../listing.js';
|
|
177
178
|
|
|
178
179
|
// Parâmetros de campanha nunca viram filtro na busca.
|
|
179
|
-
const TRACKING_PARAMS = ['gclid', 'gbraid', 'wbraid', 'gad_source', 'gad_campaignid', 'gclsrc', 'dclid', 'fbclid', 'msclkid', 'yclid', 'ttclid', 'twclid', 'epik', 'sccid', 'sc_click_id', 'li_fat_id', 'rdt_cid', 'irclickid', 'ko_click_id', 'ob_click_id', 'tblci', 'mc_cid', 'mc_eid', '_hsenc', '_hsmi', 'hsctatracking', 'mkt_tok', 'vero_id', 'wickedid'];
|
|
180
180
|
function emptyContext () {
|
|
181
181
|
return {
|
|
182
182
|
appliedFilters: [],
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Busca da Rocket no formato da listagem da Front API da Uappi
|
|
3
|
+
* (`/v2/front/url/product/listing/search`).
|
|
4
|
+
*
|
|
5
|
+
* Para o tema que quer manter a própria página de listagem (filtros, ordenação,
|
|
6
|
+
* paginação, card): troca só a fonte dos dados. A URL segue o vocabulário do tema
|
|
7
|
+
* (busca, pg, ipp, order, preco); facets da Rocket entram como `?<Facet>=v1;v2`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fetchUappiProducts } from './uappi.js';
|
|
11
|
+
|
|
12
|
+
export const TRACKING_PARAMS = ['gclid', 'gbraid', 'wbraid', 'gad_source', 'gad_campaignid', 'gclsrc', 'dclid', 'fbclid', 'msclkid', 'yclid', 'ttclid', 'twclid', 'epik', 'sccid', 'sc_click_id', 'li_fat_id', 'rdt_cid', 'irclickid', 'ko_click_id', 'ob_click_id', 'tblci', 'mc_cid', 'mc_eid', '_hsenc', '_hsmi', 'hsctatracking', 'mkt_tok', 'vero_id', 'wickedid'];
|
|
13
|
+
|
|
14
|
+
// Params do tema que não são facet. marca/filtro/atributo/selo são os filtros nativos da
|
|
15
|
+
// Uappi (ids dela): link antigo com eles não pode zerar a busca.
|
|
16
|
+
const THEME_PARAMS = ['busca', 'pg', 'ipp', 'order', 'preco', 'marca', 'filtro', 'atributo', 'selo'];
|
|
17
|
+
|
|
18
|
+
const SEARCH_ROUTE = '/busca';
|
|
19
|
+
|
|
20
|
+
function splitValues (raw) {
|
|
21
|
+
return String(raw || '').split(';').filter(v => v !== '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isForwardable (key) {
|
|
25
|
+
const lower = key.toLowerCase();
|
|
26
|
+
return !THEME_PARAMS.includes(key) && !TRACKING_PARAMS.includes(lower) && lower.indexOf('utm_') !== 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toRocketParams (query, limit) {
|
|
30
|
+
const params = {
|
|
31
|
+
q: String(query.busca || ''),
|
|
32
|
+
page: parseInt(query.pg, 10) || 1,
|
|
33
|
+
size: parseInt(query.ipp, 10) || limit,
|
|
34
|
+
sort: query.order || 'relevance'
|
|
35
|
+
};
|
|
36
|
+
if (query.preco) {
|
|
37
|
+
const [min, max] = String(query.preco).split('-');
|
|
38
|
+
if (min) params.minPrice = min;
|
|
39
|
+
if (max) params.maxPrice = max;
|
|
40
|
+
}
|
|
41
|
+
Object.keys(query).forEach((key) => {
|
|
42
|
+
const value = query[key];
|
|
43
|
+
if (value === undefined || value === null || value === '' || !isForwardable(key)) return;
|
|
44
|
+
params[key] = Array.isArray(value) ? value.join(';') : value;
|
|
45
|
+
});
|
|
46
|
+
return params;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function route (query) {
|
|
50
|
+
return { name: 'search', params: {}, query, route: SEARCH_ROUTE };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Query do link de uma opção: estado atual com o valor ligado/desligado, página zerada.
|
|
54
|
+
function toggled (query, key, value) {
|
|
55
|
+
const next = Object.assign({}, query);
|
|
56
|
+
delete next.pg;
|
|
57
|
+
const values = splitValues(next[key]);
|
|
58
|
+
const on = values.includes(value);
|
|
59
|
+
const result = on ? values.filter(v => v !== value) : values.concat(value);
|
|
60
|
+
if (result.length) next[key] = result.join(';');
|
|
61
|
+
else delete next[key];
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function facetValues (facet, query) {
|
|
66
|
+
const active = splitValues(query[facet.label]);
|
|
67
|
+
return (facet.values || []).map((v, index) => {
|
|
68
|
+
const value = String(v.value);
|
|
69
|
+
return {
|
|
70
|
+
id: index + 1,
|
|
71
|
+
nome: value,
|
|
72
|
+
label: value,
|
|
73
|
+
imagem: '',
|
|
74
|
+
imagemOriginal: '',
|
|
75
|
+
ordem: index,
|
|
76
|
+
ativo: active.includes(value),
|
|
77
|
+
rota: route(toggled(query, facet.label, value)),
|
|
78
|
+
url: ''
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toUappiListing (response, products, query, options) {
|
|
84
|
+
const opts = options || {};
|
|
85
|
+
const term = String(query.busca || '');
|
|
86
|
+
const facets = Object.values((response && response.facets) || {});
|
|
87
|
+
const baseQuery = Object.assign({}, query);
|
|
88
|
+
delete baseQuery.pg;
|
|
89
|
+
|
|
90
|
+
const marcas = [];
|
|
91
|
+
const atributos = [];
|
|
92
|
+
const filtrosPreco = [];
|
|
93
|
+
facets.forEach((facet, index) => {
|
|
94
|
+
if (facet.type === 'range') {
|
|
95
|
+
if (facet.min != null && facet.max != null) {
|
|
96
|
+
const de = Math.floor(facet.min);
|
|
97
|
+
const ate = Math.ceil(facet.max);
|
|
98
|
+
filtrosPreco.push({ de, ate, ativo: !!query.preco, rota: route(Object.assign({}, baseQuery, { preco: `${de}-${ate}` })), url: '' });
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Disponibilidade (0/1) não tem par na listagem da Uappi.
|
|
103
|
+
if (facet.type !== 'terms' || facet.field === 'hasStock' || !(facet.values || []).length) return;
|
|
104
|
+
const values = facetValues(facet, query);
|
|
105
|
+
if (facet.field === 'brand.name') marcas.push(...values);
|
|
106
|
+
else atributos.push({ id: index + 1, nome: facet.label, tipo: 'texto', valores: values });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const ordenacao = ((response && response.sortOptions) || []).map(option => ({
|
|
110
|
+
label: option.label,
|
|
111
|
+
title: option.description || option.label,
|
|
112
|
+
atual: (query.order || 'relevance') === option.id,
|
|
113
|
+
rota: route(Object.assign({}, baseQuery, { order: option.id })),
|
|
114
|
+
url: ''
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const size = (response && response.size) || parseInt(query.ipp, 10) || opts.limit;
|
|
118
|
+
const page = (response && response.page) || 1;
|
|
119
|
+
const title = (term ? term.charAt(0).toUpperCase() + term.slice(1) : 'Busca') + (opts.titleSuffix || '');
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
estrutura: {
|
|
123
|
+
nivel: 'product/listing/search',
|
|
124
|
+
seo: { title, titleSocial: title, description: '', descriptionSocial: '', keywords: '', indexar: false },
|
|
125
|
+
breadcrumb: [
|
|
126
|
+
{ label: 'Home', atual: false, rota: { name: 'home', params: {}, query: {}, route: '/' }, url: '/' },
|
|
127
|
+
{ label: 'Busca', atual: true, rota: route({ busca: term }), url: '' }
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
conteudo: {
|
|
131
|
+
busca: { texto: term, sugestao: [] },
|
|
132
|
+
layoutPersonalizado: { layout: '', codigoCss: '' },
|
|
133
|
+
produtos: products,
|
|
134
|
+
detalhes: {
|
|
135
|
+
categorias: { niveisAnteriores: [], atual: [], irmas: [], filhas: [] },
|
|
136
|
+
marcas,
|
|
137
|
+
filtros: [],
|
|
138
|
+
filtrosPreco,
|
|
139
|
+
filtrosDescontoPrecoPor: [],
|
|
140
|
+
selos: [],
|
|
141
|
+
atributos,
|
|
142
|
+
ordenacao
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
info: {
|
|
146
|
+
filtros: { busca: term },
|
|
147
|
+
offset: (page - 1) * size,
|
|
148
|
+
limit: size,
|
|
149
|
+
exibindo: products.length,
|
|
150
|
+
total: (response && response.total) || 0
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Busca na Rocket + dados dos produtos na Uappi, já no formato da listagem.
|
|
157
|
+
* `rocketSearch` leva o que o evento `search` precisa: no SSR o dispatchEvent é no-op,
|
|
158
|
+
* então quem chama dispara com `dispatchListingSearch` já no cliente.
|
|
159
|
+
*/
|
|
160
|
+
export async function rocketSearchListing ({ $rocket, $axios, query, limit, titleSuffix }) {
|
|
161
|
+
const start = Date.now();
|
|
162
|
+
const response = await $rocket.search(toRocketParams(query, limit));
|
|
163
|
+
const responseTime = Date.now() - start;
|
|
164
|
+
$rocket.rememberSearchPrices(response.products);
|
|
165
|
+
const products = await fetchUappiProducts($axios, (response.products || []).map(p => p.id));
|
|
166
|
+
const listing = toUappiListing(response, products, query, { limit, titleSuffix });
|
|
167
|
+
listing.rocketSearch = { query: String(query.busca || ''), total: response.total || 0, responseTime, sent: false };
|
|
168
|
+
return listing;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function dispatchListingSearch ($rocket, listing) {
|
|
172
|
+
const info = listing && listing.rocketSearch;
|
|
173
|
+
if (!info || info.sent || !info.query || typeof window === 'undefined') return;
|
|
174
|
+
info.sent = true;
|
|
175
|
+
$rocket.dispatchEvent('search', {
|
|
176
|
+
query: info.query,
|
|
177
|
+
resultsCount: info.total,
|
|
178
|
+
meta: { query: info.query, resultsCount: info.total, responseTime: info.responseTime }
|
|
179
|
+
}).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function dispatchListingClick ($rocket, listing, product, position) {
|
|
183
|
+
if (!product || product.id == null) return;
|
|
184
|
+
$rocket.dispatchEvent('click', {
|
|
185
|
+
productId: String(product.id),
|
|
186
|
+
position,
|
|
187
|
+
meta: { source: 'search-page', query: (listing && listing.rocketSearch && listing.rocketSearch.query) || '' }
|
|
188
|
+
}).catch(() => {});
|
|
189
|
+
}
|
package/runtime/plugin.js
CHANGED
|
@@ -51,10 +51,16 @@ export default function ({ app, store, $axios }, inject) {
|
|
|
51
51
|
// chega hidratado — não há mutation no cliente para ouvir.
|
|
52
52
|
if (store && store.state.dadosPageAtual) applyPage(store.state.dadosPageAtual);
|
|
53
53
|
|
|
54
|
+
// Cliente logado (ROC-656): o tema guarda o retorno de checkout/user (`{ id, logado }`) no
|
|
55
|
+
// Vuex — `setUser` no SSR, então chega hidratado — e commita `setUser` de novo no login e na
|
|
56
|
+
// troca de perfil, que não recarregam a página.
|
|
57
|
+
if (store && store.state.user) rocket.trackLogin(store.state.user).catch(() => {});
|
|
58
|
+
|
|
54
59
|
// Navegação SPA: o tema commita setDadosPageAtual quando a página nova carrega os dados.
|
|
55
60
|
if (store) {
|
|
56
61
|
store.subscribe((mutation) => {
|
|
57
62
|
if (mutation.type === 'setDadosPageAtual') applyPage(mutation.payload);
|
|
63
|
+
else if (mutation.type === 'setUser') rocket.trackLogin(mutation.payload).catch(() => {});
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
|
package/runtime/rocket.js
CHANGED
|
@@ -22,6 +22,7 @@ const PENDING_CART_EVENTS_KEY = 'rs_pending_add_to_cart_events';
|
|
|
22
22
|
const ANON_SESSION_KEY = 'rs_anon_session_id';
|
|
23
23
|
const ANON_SESSION_TTL = 30 * 60 * 1000;
|
|
24
24
|
const LAST_SECTION_KEY = 'rs_last_section';
|
|
25
|
+
const LOGIN_SENT_KEY = 'rs_login_sent';
|
|
25
26
|
const DEBUG_TOKEN_KEY = 'rs_debug_token';
|
|
26
27
|
const SHOWCASE_DEBUG_KEY = 'rs_showcase_debug';
|
|
27
28
|
// 2h — em sincronia com DEBUG_SESSION_TTL_SECONDS no backend.
|
|
@@ -489,6 +490,22 @@ export function createRocket (config) {
|
|
|
489
490
|
rocket.dispatchEvent('page_view', { meta: { pageType: section, fromSection: prev || null } }).catch(() => {});
|
|
490
491
|
};
|
|
491
492
|
|
|
493
|
+
// Cliente logado → 'login' com o id do cliente na Uappi (o mesmo `cliente.id` dos pedidos),
|
|
494
|
+
// uma vez por (sessão, cliente) na aba (ROC-656). `user` é a resposta da Front API em
|
|
495
|
+
// checkout/user, checkout/login e checkout/login-multiple-profile: `{ id, logado, ... }`.
|
|
496
|
+
// A Uappi não tem verificador de comprador: o vínculo fica `declared`. A marca só é gravada
|
|
497
|
+
// depois de entregue — envio perdido sai de novo na próxima resposta com o cliente.
|
|
498
|
+
rocket.trackLogin = async function (user) {
|
|
499
|
+
const customerId = user && user.logado !== false ? toPositiveNumber(user.id) : null;
|
|
500
|
+
if (!isBrowser || !customerId) return;
|
|
501
|
+
const sessionId = await rocket.getSessionId();
|
|
502
|
+
if (!sessionId) return;
|
|
503
|
+
const mark = sessionId + ':' + customerId;
|
|
504
|
+
if (storageGet(LOGIN_SENT_KEY) === mark) return;
|
|
505
|
+
const res = await rocket.dispatchEvent('login', { customerId });
|
|
506
|
+
if (res && res.ok) storageSet(LOGIN_SENT_KEY, mark);
|
|
507
|
+
};
|
|
508
|
+
|
|
492
509
|
rocket.fireProductView = function () {
|
|
493
510
|
if (!page.productId) return;
|
|
494
511
|
const viewData = { productId: page.productId };
|