@rocketsearch/vue-uappi 0.1.2 → 0.1.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/README.md CHANGED
@@ -29,5 +29,6 @@ Use os componentes em PascalCase (`<RocketSearch>`, `<RocketShowcase position="t
29
29
 
30
30
  Landing page ("Personalizado" no painel): `<RocketLanding>` mostra todas as vitrines da landing, na ordem do
31
31
  painel. O identificador vem do nome da rota (`/black-friday` → `black-friday`) ou de `custom-key="..."`.
32
+ Banners da landing: `<RocketBanner page-type="custom" />`, com a mesma regra do identificador.
32
33
 
33
34
  Documentação completa: https://api.rocketsearch.com.br/sdk/uappi/docs.html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocketsearch/vue-uappi",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Rocket Search para storefronts Uappi em Vue 3 + Vite + Pinia: busca, autocomplete, vitrines, banners e eventos",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  :class="['rocket-search', 'rocket-search__banner-slot', { 'rocket-search--empty': !waiting && !banners.length }]"
4
- :data-rocket-banner-position="position"
4
+ :data-rocket-banner-position="position || undefined"
5
5
  >
6
6
  <!-- Skeleton só com proporção declarada: sem saber a altura, ele é que causaria o pulo. -->
7
7
  <div v-if="waiting && ratio" class="rocket-search__banner-skeleton" :style="{ aspectRatio: ratioCss }" aria-hidden="true" />
@@ -30,12 +30,14 @@ import '../css/banner.css';
30
30
  import RocketBannerItem from './banner/RocketBannerItem.vue';
31
31
  import { exactWidth, isHeroSlot } from '../banner.js';
32
32
 
33
+ const positionOf = b => (b && ((b.placement && b.placement.position) || b.position)) || '';
34
+
33
35
  export default {
34
36
  name: 'RocketBanner',
35
37
  components: { RocketBannerItem },
36
38
  props: {
37
- // placement.position cadastrada no painel (hero, top, middle...).
38
- position: { type: String, required: true },
39
+ // placement.position cadastrada no painel (hero, top, middle...). Obrigatória fora da landing.
40
+ position: { type: String, default: '' },
39
41
  // eager: busca assim que a página tem contexto (hero/LCP). ondemand: só quando o slot
40
42
  // chega perto da viewport.
41
43
  loading: {
@@ -45,6 +47,9 @@ export default {
45
47
  },
46
48
  // Força o pageType; sem ele vem da página atual (currentPageData do tema).
47
49
  pageType: { type: String, default: '' },
50
+ // Landing (page-type="custom"): identificador no painel; sem ele, o nome da rota
51
+ // (/black-friday -> black-friday). A landing não tem posição: vêm todos os banners dela.
52
+ customKey: { type: String, default: '' },
48
53
  limit: { type: Number, default: 0 },
49
54
  // Proporção do slot ("16/9", "3:1") — reserva a caixa e liga o skeleton.
50
55
  ratio: { type: String, default: '' },
@@ -63,11 +68,22 @@ export default {
63
68
  };
64
69
  },
65
70
  computed: {
71
+ landingKey () {
72
+ if (this.pageType !== 'custom') return '';
73
+ if (this.customKey) return this.customKey;
74
+ return ((this.$route && this.$route.path) || location.pathname).split('/').filter(Boolean).pop() || '';
75
+ },
66
76
  ratioCss () {
67
77
  return String(this.ratio).trim().replace(':', ' / ');
68
78
  }
69
79
  },
70
80
  watch: {
81
+ // Mesma página, outra landing (rota dinâmica reaproveita o componente): busca de novo.
82
+ landingKey () {
83
+ if (!this.requested) return;
84
+ this.requested = false;
85
+ this.tryLoad();
86
+ },
71
87
  // O contexto da página chega pelo currentPageData do tema (o plugin traduz para $rocket.setPage).
72
88
  '$rocket.currentPage.value' () {
73
89
  if (this.loading === 'eager' || this.visible) this.tryLoad();
@@ -99,16 +115,23 @@ export default {
99
115
  if (this.$el && this.$el.clientWidth) {
100
116
  this.width = exactWidth(this.$el.clientWidth, window.devicePixelRatio);
101
117
  }
102
- const list = await this.$rocket.getBannersByContext(this.pageType || undefined, { position: this.position });
103
- const mine = list.filter(b => ((b.placement && b.placement.position) || b.position) === this.position);
118
+ const key = this.landingKey;
119
+ const list = this.pageType === 'custom'
120
+ ? (key ? await this.$rocket.getBannersByContext('custom', { customKey: key }) : [])
121
+ : await this.$rocket.getBannersByContext(this.pageType || undefined, { position: this.position });
122
+ // Troca rápida de landing: a resposta da anterior não sobrescreve a atual.
123
+ if (key !== this.landingKey) return;
124
+ const mine = this.pageType === 'custom' ? list : list.filter(b => positionOf(b) === this.position);
104
125
  this.banners = this.limit > 0 ? mine.slice(0, this.limit) : mine;
105
126
  this.waiting = false;
106
127
  },
107
128
 
108
129
  itemProps (index) {
109
- const hero = isHeroSlot(this.position) && index === 0;
130
+ // Na landing o slot é a posição do próprio banner.
131
+ const position = this.position || positionOf(this.banners[index]);
132
+ const hero = isHeroSlot(position) && index === 0;
110
133
  return {
111
- position: this.position,
134
+ position,
112
135
  slideIndex: index,
113
136
  loading: hero ? 'eager' : 'lazy',
114
137
  fetchPriority: hero ? 'high' : '',
@@ -50,7 +50,10 @@ export default {
50
50
  },
51
51
  methods: {
52
52
  async load () {
53
- this.showcases = this.key ? await this.$rocket.getPageShowcases('custom', this.key) : [];
53
+ const key = this.key;
54
+ const showcases = key ? await this.$rocket.getPageShowcases('custom', key, true) : [];
55
+ // Troca rápida de rota: a resposta da landing anterior não sobrescreve a atual.
56
+ if (key === this.key) this.showcases = showcases;
54
57
  }
55
58
  }
56
59
  };
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div
3
- v-if="showcase || loading || debug"
4
- :class="['rocket-search', { 'rocket-search--empty': !showcase && !loading }]"
5
- :data-rocket-showcase-position="position"
3
+ v-if="showcase || loading || debug || restIds.length"
4
+ :class="['rocket-search', { 'rocket-search--empty': !showcase && !loading && !restIds.length, 'rocket-search--stacked': stacked }]"
5
+ :data-rocket-showcase-position="stacked ? null : position"
6
6
  >
7
7
  <div v-if="loading" class="rocket-search__showcase rocket-search__showcase--skeleton" :style="columnsStyle">
8
8
  <div class="rocket-search__showcase__title rocket-search__product-placeholder__item" />
@@ -47,6 +47,26 @@
47
47
  <button type="button" class="rocket-search__swiper-button-next" aria-label="Próximos" @click="slide(1)" />
48
48
  </slot>
49
49
  </section>
50
+
51
+ <!-- Demais vitrines da mesma posição, na ordem da API: cada uma medida por conta própria. -->
52
+ <RocketShowcase
53
+ v-for="id in restIds"
54
+ :key="id"
55
+ :position="position"
56
+ :page-type="pageType"
57
+ :custom-key="customKey"
58
+ :showcase-id="id"
59
+ :media-size="mediaSize"
60
+ stacked
61
+ @products-loaded="$emit('products-loaded', $event)"
62
+ >
63
+ <template v-if="$slots.default" #default="scope">
64
+ <slot v-bind="scope" />
65
+ </template>
66
+ <template v-if="$slots.product" #product="scope">
67
+ <slot name="product" v-bind="scope" />
68
+ </template>
69
+ </RocketShowcase>
50
70
  </div>
51
71
  </template>
52
72
 
@@ -71,12 +91,15 @@ export default {
71
91
  pageType: { type: String, default: '' },
72
92
  // Landing (page-type="custom"): identificador cadastrado no painel em "Personalizado".
73
93
  customKey: { type: String, default: '' },
74
- // Vitrine específica (a landing tem várias na mesma posição); sem ele, a primeira da posição.
94
+ // Vitrine específica (a landing tem várias na mesma posição); sem ele, todas as da posição.
75
95
  showcaseId: { type: [String, Number], default: null },
76
- mediaSize: { type: String, default: 'medium' }
96
+ mediaSize: { type: String, default: 'medium' },
97
+ // Uso interno: vitrine empilhada abaixo da primeira da posição. Sem o atributo de posição,
98
+ // o debug contorna a posição uma vez só.
99
+ stacked: { type: Boolean, default: false }
77
100
  },
78
101
  data () {
79
- return { showcase: null, products: [], loading: false, debug: false };
102
+ return { showcase: null, products: [], restIds: [], loading: false, debug: false };
80
103
  },
81
104
  computed: {
82
105
  columns () {
@@ -122,10 +145,13 @@ export default {
122
145
  this.started = true;
123
146
  try {
124
147
  const showcases = await this.$rocket.getPageShowcases(this.pageType || undefined, this.customKey || undefined);
125
- const showcase = showcases.find(s => this.showcaseId != null
126
- ? String(s.id) === String(this.showcaseId)
127
- : ((s.placement && s.placement.position) || s.position) === this.position);
148
+ const inPosition = showcases.filter(s => ((s.placement && s.placement.position) || s.position) === this.position);
149
+ const showcase = this.showcaseId != null
150
+ ? showcases.find(s => String(s.id) === String(this.showcaseId))
151
+ : inPosition[0];
128
152
  if (!showcase) return;
153
+ // Posição sem showcaseId: este slot mostra a primeira e empilha as demais logo abaixo.
154
+ if (this.showcaseId == null) this.restIds = inPosition.slice(1).map(s => s.id);
129
155
  // Banner autônomo: render direto, sem produtos, skeleton nem impressão de vitrine
130
156
  // (o RocketBannerItem mede o criativo).
131
157
  if (isBannerItem(showcase)) {
@@ -6,6 +6,11 @@
6
6
  margin-bottom: 60px;
7
7
  }
8
8
 
9
+ /* Segunda vitrine em diante na mesma posição: o mesmo respiro entre elas. */
10
+ .rocket-search--stacked {
11
+ margin-top: 60px;
12
+ }
13
+
9
14
  .rocket-search__showcase {
10
15
  --showcase-gap: 16px;
11
16
  position: relative;
package/runtime/rocket.js CHANGED
@@ -122,7 +122,10 @@ export function createRocket (config) {
122
122
 
123
123
  rocket.setPage = function (next) {
124
124
  page = Object.assign({ section: null, productId: null, categoryId: null, product: null }, next);
125
- pageShowcases = {};
125
+ // A landing (custom) não depende do contexto da página e fica: senão, um setPage entre a busca
126
+ // do RocketLanding e a montagem das vitrines dele dispararia uma segunda chamada. Ela se
127
+ // renova quando o RocketLanding monta (refresh).
128
+ Object.keys(pageShowcases).forEach((k) => { if (k.indexOf('custom|') !== 0) delete pageShowcases[k]; });
126
129
  };
127
130
  rocket.getPage = () => page;
128
131
 
@@ -320,15 +323,16 @@ export function createRocket (config) {
320
323
 
321
324
  // Uma chamada por página, repartida entre os slots (<RocketShowcase position>) — na Linx
322
325
  // era o fetchShowCases único distribuindo por data-rocket-showcase-position. Por pageType +
323
- // customKey: a landing e o contexto da página são chamadas distintas. Zera no setPage.
326
+ // customKey: a landing e o contexto da página são chamadas distintas. O setPage zera só o contexto.
324
327
  let pageShowcases = {};
325
- rocket.getPageShowcases = function (type, customKey) {
328
+ rocket.getPageShowcases = function (type, customKey, refresh) {
326
329
  const key = (type || '') + '|' + (customKey || '');
327
- if (!pageShowcases[key]) pageShowcases[key] = rocket.getShowcasesByContext(type, customKey).catch(() => []);
330
+ if (refresh || !pageShowcases[key]) pageShowcases[key] = rocket.getShowcasesByContext(type, customKey).catch(() => []);
328
331
  return pageShowcases[key];
329
332
  };
330
333
 
331
334
  // Só banners (DTO banner-native). Gated pela flag `banners`: loja sem ela recebe [].
335
+ // options.customKey: identificador da landing (pageType=custom); sem ele a API responde 400.
332
336
  rocket.getBannersByContext = async function (type, options) {
333
337
  const pageType = resolvePageType(type);
334
338
  if (!pageType) return [];
@@ -336,6 +340,7 @@ export function createRocket (config) {
336
340
  try {
337
341
  const sessionId = await getSessionIdWithTimeout();
338
342
  const params = { storeId: config.storeId, pageType, sessionId, position: opts.position };
343
+ if (pageType === 'custom') params.customKey = opts.customKey;
339
344
  const res = await fetch(config.apiUrl + '/banners/context?' + query(params), { headers: headers() });
340
345
  if (!res.ok) return [];
341
346
  const banners = await res.json();