orionplace-svelte 0.6.1 → 0.6.2
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/package.json +1 -1
- package/templates/src/lib/components/orionplace/blog/BlogArticle.README.md +2 -0
- package/templates/src/lib/components/orionplace/blog/BlogArticle.svelte +239 -70
- package/templates/src/lib/components/orionplace/products/ProductCard.svelte +17 -0
- package/templates/src/lib/components/orionplace/products/ProductGroup.svelte +6 -0
- package/templates/src/lib/utils/orionplace/blogArticleHtml.ts +1 -0
- package/templates/src/lib/utils/orionplace/blogProductPlacements.ts +105 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-svelte",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "SvelteKit 2 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and Russian, plus an Android app build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,8 @@ Article videos may come as the backend's own paths (`/ps3/...`). In an Android a
|
|
|
8
8
|
|
|
9
9
|
The cover and the images of the text open larger: a click, Enter or Space on one opens `ProductReviewImageViewer` at that image, with the article's other images a step away and each image's caption (its `figcaption`, else its `title`, else its `alt`) under it. The images get `role="button"`, a keyboard stop and an «Open image» name; a zoom-in cursor marks them. Images inside a link, a button or a product placement keep their own action. The helper is `src/lib/utils/orionplace/blogImageViewer.ts`.
|
|
10
10
|
|
|
11
|
+
Products the author places in the text appear at those positions: an empty block with `data-product-id="12"` shows one product card, `data-product-ids="12,15,9"` a row of up to 24 cards. The block keeps the editor's float or centred style, and `data-product-hidden-badges` (`new`, `popular`, `good-deal`) turns card badges off. Products missing from `related_products` are loaded with `products_search` and `filters.id_in`, up to 35 IDs per request, and stored with `store.setProduct()`; a block whose products are not found stays hidden. Article text styles do not reach the cards, and in a narrow article a floated card stands alone in the middle. The helper is `src/lib/utils/orionplace/blogProductPlacements.ts`.
|
|
12
|
+
|
|
11
13
|
Source: `src/lib/components/orionplace/blog/BlogArticle.svelte`.
|
|
12
14
|
|
|
13
15
|
## Inputs
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
nextTick,
|
|
6
6
|
onMounted,
|
|
7
7
|
ref,
|
|
8
|
+
shallowRef,
|
|
8
9
|
watch,
|
|
9
10
|
} from '@/lib/orionplace/reactivity.svelte';
|
|
10
11
|
import { useOrionplaceStore } from '@/lib/stores/orionplace.svelte';
|
|
@@ -28,6 +29,13 @@
|
|
|
28
29
|
import { htmlTextParagraphs } from '@/lib/utils/orionplace/htmlText';
|
|
29
30
|
import { APP_ORIGIN, appMediaUrl } from '@/lib/utils/orionplace/nativeApp';
|
|
30
31
|
import { requestBlogPost } from '@/lib/utils/orionplace/blogLayout';
|
|
32
|
+
import {
|
|
33
|
+
blogPlacementProductIds,
|
|
34
|
+
blogPlacementRequests,
|
|
35
|
+
blogProductPlacements,
|
|
36
|
+
} from '@/lib/utils/orionplace/blogProductPlacements';
|
|
37
|
+
import type { IBlogProductPlacement } from '@/lib/utils/orionplace/blogProductPlacements';
|
|
38
|
+
import type { IProductSearchSeller } from '@/lib/interfaces/orionplace/products/IProduct';
|
|
31
39
|
import type { IBlogPost } from '@/lib/interfaces/orionplace/blog/IBlogApi';
|
|
32
40
|
import type { IProductSecondaryVariantRequest } from '@/lib/interfaces/orionplace/products/IProductSecondaryVariantRequest';
|
|
33
41
|
import type {
|
|
@@ -38,6 +46,7 @@
|
|
|
38
46
|
} from '@/lib/interfaces/orionplace/products/IProduct';
|
|
39
47
|
import BlogComments from '@/lib/components/orionplace/blog/BlogComments.svelte';
|
|
40
48
|
import ProductGroup from '@/lib/components/orionplace/products/ProductGroup.svelte';
|
|
49
|
+
import ProductCard from '@/lib/components/orionplace/products/ProductCard.svelte';
|
|
41
50
|
import '@/lib/components/orionplace/shared/features.css';
|
|
42
51
|
import OrionplaceEmptyState from '@/lib/components/orionplace/shared/OrionplaceEmptyState.svelte';
|
|
43
52
|
import ProductReviewImageViewer from '@/lib/components/orionplace/products/ProductReviewImageViewer.svelte';
|
|
@@ -153,6 +162,8 @@
|
|
|
153
162
|
const accessChanged = ref(false);
|
|
154
163
|
const coverFailed = ref(false);
|
|
155
164
|
const productIds = ref<number[]>([]);
|
|
165
|
+
// Product cards the author placed in the text (see blogProductPlacements).
|
|
166
|
+
const placements = shallowRef<IBlogProductPlacement[]>([]);
|
|
156
167
|
const wasShown = ref(false);
|
|
157
168
|
const settled = ref(false);
|
|
158
169
|
const labels = computed(() => blogLabels(selectionStore.localeCode));
|
|
@@ -300,6 +311,57 @@
|
|
|
300
311
|
wasShown.value = true;
|
|
301
312
|
showProducts(inputs.initialPost.related_products);
|
|
302
313
|
}
|
|
314
|
+
function placementShown(placement: IBlogProductPlacement) {
|
|
315
|
+
return placement.productIds.some((id) => productStore.getProduct(id, 'products'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Placed products that no list loaded yet come with one request per 35.
|
|
319
|
+
async function loadPlacementProducts(ids: number[]) {
|
|
320
|
+
const missing = ids.filter((id) => !productStore.getProduct(id, 'products'));
|
|
321
|
+
|
|
322
|
+
for (const chunk of blogPlacementRequests(missing)) {
|
|
323
|
+
try {
|
|
324
|
+
const data = await api.request<{
|
|
325
|
+
products?: { data?: IProduct[] };
|
|
326
|
+
selects?: { sellers?: IProductSearchSeller[] };
|
|
327
|
+
}>('products_search', {
|
|
328
|
+
filters: { id_in: chunk.join(',') },
|
|
329
|
+
paginate: { page: 1, pp_items: chunk.length },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
for (const product of data?.products?.data ?? []) {
|
|
333
|
+
productStore.setProduct(product, 'products', data?.selects?.sellers ?? []);
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// A placement without its products stays empty and hidden.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
watch(
|
|
342
|
+
[() => html.value, () => mounted.value, () => post.value?.id],
|
|
343
|
+
() => {
|
|
344
|
+
void nextTick(() => {
|
|
345
|
+
placements.value = mounted.value ? blogProductPlacements(article.value) : [];
|
|
346
|
+
void loadPlacementProducts(blogPlacementProductIds(placements.value));
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
{ immediate: true },
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Moves the cards into their placement block in the article text.
|
|
353
|
+
function portal(node: HTMLElement, target: HTMLElement) {
|
|
354
|
+
target.append(node);
|
|
355
|
+
return {
|
|
356
|
+
update(next: HTMLElement) {
|
|
357
|
+
next.append(node);
|
|
358
|
+
},
|
|
359
|
+
destroy() {
|
|
360
|
+
node.remove();
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
303
365
|
function openableImages() {
|
|
304
366
|
return blogViewerImages([coverImage.value, article.value]);
|
|
305
367
|
}
|
|
@@ -473,6 +535,94 @@
|
|
|
473
535
|
{/each}
|
|
474
536
|
</div>
|
|
475
537
|
{/if}
|
|
538
|
+
{#each placements.value as placement, index (index)}
|
|
539
|
+
{#if placementShown(placement)}
|
|
540
|
+
<div
|
|
541
|
+
use:portal={placement.element}
|
|
542
|
+
data-blog-placement={placement.layout}
|
|
543
|
+
>
|
|
544
|
+
{#if placement.layout === 'single'}
|
|
545
|
+
<ProductCard
|
|
546
|
+
productId={placement.productIds[0]!}
|
|
547
|
+
hiddenBadges={placement.hiddenBadges}
|
|
548
|
+
disabled={inputs.productActionsDisabled}
|
|
549
|
+
onSelect={($event) => {
|
|
550
|
+
emit('selectProduct', $event);
|
|
551
|
+
}}
|
|
552
|
+
onSelectSeller={($event) => {
|
|
553
|
+
emit('selectSeller', $event);
|
|
554
|
+
}}
|
|
555
|
+
onSelectVariant={($event) => {
|
|
556
|
+
emit('selectVariant', $event);
|
|
557
|
+
}}
|
|
558
|
+
onRequestSecondaryVariants={($event) => {
|
|
559
|
+
emit('requestSecondaryVariants', $event);
|
|
560
|
+
}}
|
|
561
|
+
onSelectSecondaryVariant={($event) => {
|
|
562
|
+
emit('selectSecondaryVariant', $event);
|
|
563
|
+
}}
|
|
564
|
+
onToggleFavorite={($event) => {
|
|
565
|
+
emit('toggleFavorite', $event);
|
|
566
|
+
}}
|
|
567
|
+
onAdd={($event) => {
|
|
568
|
+
emit('add', $event);
|
|
569
|
+
}}
|
|
570
|
+
onRemove={($event) => {
|
|
571
|
+
emit('remove', $event);
|
|
572
|
+
}}
|
|
573
|
+
onSubscribe={($event) => {
|
|
574
|
+
emit('subscribe', $event);
|
|
575
|
+
}}
|
|
576
|
+
onUnsubscribe={($event) => {
|
|
577
|
+
emit('unsubscribe', $event);
|
|
578
|
+
}}
|
|
579
|
+
onPreorder={($event) => {
|
|
580
|
+
emit('preorder', $event);
|
|
581
|
+
}}
|
|
582
|
+
/>
|
|
583
|
+
{:else}
|
|
584
|
+
<ProductGroup
|
|
585
|
+
productIds={placement.productIds}
|
|
586
|
+
hiddenBadges={placement.hiddenBadges}
|
|
587
|
+
disabled={inputs.productActionsDisabled}
|
|
588
|
+
onSelect={($event) => {
|
|
589
|
+
emit('selectProduct', $event);
|
|
590
|
+
}}
|
|
591
|
+
onSelectSeller={($event) => {
|
|
592
|
+
emit('selectSeller', $event);
|
|
593
|
+
}}
|
|
594
|
+
onSelectVariant={($event) => {
|
|
595
|
+
emit('selectVariant', $event);
|
|
596
|
+
}}
|
|
597
|
+
onRequestSecondaryVariants={($event) => {
|
|
598
|
+
emit('requestSecondaryVariants', $event);
|
|
599
|
+
}}
|
|
600
|
+
onSelectSecondaryVariant={($event) => {
|
|
601
|
+
emit('selectSecondaryVariant', $event);
|
|
602
|
+
}}
|
|
603
|
+
onToggleFavorite={($event) => {
|
|
604
|
+
emit('toggleFavorite', $event);
|
|
605
|
+
}}
|
|
606
|
+
onAdd={($event) => {
|
|
607
|
+
emit('add', $event);
|
|
608
|
+
}}
|
|
609
|
+
onRemove={($event) => {
|
|
610
|
+
emit('remove', $event);
|
|
611
|
+
}}
|
|
612
|
+
onSubscribe={($event) => {
|
|
613
|
+
emit('subscribe', $event);
|
|
614
|
+
}}
|
|
615
|
+
onUnsubscribe={($event) => {
|
|
616
|
+
emit('unsubscribe', $event);
|
|
617
|
+
}}
|
|
618
|
+
onPreorder={($event) => {
|
|
619
|
+
emit('preorder', $event);
|
|
620
|
+
}}
|
|
621
|
+
/>
|
|
622
|
+
{/if}
|
|
623
|
+
</div>
|
|
624
|
+
{/if}
|
|
625
|
+
{/each}
|
|
476
626
|
|
|
477
627
|
{#if !full.value}
|
|
478
628
|
<div
|
|
@@ -670,144 +820,145 @@
|
|
|
670
820
|
clear: both;
|
|
671
821
|
}
|
|
672
822
|
|
|
673
|
-
|
|
823
|
+
/* Product cards placed in the text keep their own look: the text rules skip placement blocks. */
|
|
824
|
+
.op-feature-prose :global(:where(p, ul, ol, blockquote, pre, figure):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
674
825
|
margin: 1rem 0;
|
|
675
826
|
}
|
|
676
827
|
|
|
677
828
|
/* A new section or wide block starts below a floated image. */
|
|
678
829
|
.op-feature-prose
|
|
679
|
-
:global(:where(h1, h2, h3, h4, h5, h6, figure, table, pre)) {
|
|
830
|
+
:global(:where(h1, h2, h3, h4, h5, h6, figure, table, pre):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
680
831
|
clear: both;
|
|
681
832
|
}
|
|
682
833
|
|
|
683
|
-
.op-feature-prose :global(:where(h1, h2, h3, h4, h5, h6)) {
|
|
834
|
+
.op-feature-prose :global(:where(h1, h2, h3, h4, h5, h6):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
684
835
|
margin: 2rem 0 1rem;
|
|
685
836
|
font-weight: 500;
|
|
686
837
|
line-height: 1.3;
|
|
687
838
|
}
|
|
688
839
|
|
|
689
|
-
.op-feature-prose :global(h1) {
|
|
840
|
+
.op-feature-prose :global(h1:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
690
841
|
font-size: 1.875rem;
|
|
691
842
|
}
|
|
692
843
|
|
|
693
|
-
.op-feature-prose :global(h2) {
|
|
844
|
+
.op-feature-prose :global(h2:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
694
845
|
font-size: 1.5rem;
|
|
695
846
|
}
|
|
696
847
|
|
|
697
|
-
.op-feature-prose :global(h3) {
|
|
848
|
+
.op-feature-prose :global(h3:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
698
849
|
font-size: 1.25rem;
|
|
699
850
|
}
|
|
700
851
|
|
|
701
|
-
.op-feature-prose :global(h4) {
|
|
852
|
+
.op-feature-prose :global(h4:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
702
853
|
font-size: 1.125rem;
|
|
703
854
|
}
|
|
704
855
|
|
|
705
|
-
.op-feature-prose :global(h5) {
|
|
856
|
+
.op-feature-prose :global(h5:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
706
857
|
font-size: 1rem;
|
|
707
858
|
}
|
|
708
859
|
|
|
709
|
-
.op-feature-prose :global(h6) {
|
|
860
|
+
.op-feature-prose :global(h6:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
710
861
|
font-size: 0.875rem;
|
|
711
862
|
text-transform: uppercase;
|
|
712
863
|
letter-spacing: 0.08em;
|
|
713
864
|
}
|
|
714
865
|
|
|
715
|
-
.op-feature-prose :global(:where(strong, b)) {
|
|
866
|
+
.op-feature-prose :global(:where(strong, b):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
716
867
|
font-weight: 500;
|
|
717
868
|
}
|
|
718
869
|
|
|
719
|
-
.op-feature-prose :global(:where(sub, sup)) {
|
|
870
|
+
.op-feature-prose :global(:where(sub, sup):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
720
871
|
font-size: 0.75em;
|
|
721
872
|
line-height: 0;
|
|
722
873
|
}
|
|
723
874
|
|
|
724
|
-
.op-feature-prose :global(mark) {
|
|
875
|
+
.op-feature-prose :global(mark:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
725
876
|
color: inherit;
|
|
726
877
|
background: var(--orionplace-attention-surface);
|
|
727
878
|
}
|
|
728
879
|
|
|
729
|
-
.op-feature-prose :global(a) {
|
|
880
|
+
.op-feature-prose :global(a:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
730
881
|
color: var(--orionplace-text);
|
|
731
882
|
text-decoration: none;
|
|
732
883
|
border-bottom: 1px solid currentColor;
|
|
733
884
|
}
|
|
734
885
|
|
|
735
|
-
.op-feature-prose :global(a:not([href])) {
|
|
886
|
+
.op-feature-prose :global(a:not([href]):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
736
887
|
border-bottom: 0;
|
|
737
888
|
}
|
|
738
889
|
|
|
739
|
-
.op-feature-prose :global(a:focus-visible) {
|
|
890
|
+
.op-feature-prose :global(a:focus-visible:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
740
891
|
color: var(--orionplace-accent);
|
|
741
892
|
outline: none;
|
|
742
893
|
}
|
|
743
894
|
|
|
744
895
|
@media (hover: hover) {
|
|
745
|
-
.op-feature-prose :global(a:hover) {
|
|
896
|
+
.op-feature-prose :global(a:hover:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
746
897
|
color: var(--orionplace-accent);
|
|
747
898
|
outline: none;
|
|
748
899
|
}
|
|
749
900
|
}
|
|
750
901
|
|
|
751
|
-
.op-feature-prose :global(:where(ul, ol)) {
|
|
902
|
+
.op-feature-prose :global(:where(ul, ol):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
752
903
|
padding-left: 1.5rem;
|
|
753
904
|
}
|
|
754
905
|
|
|
755
|
-
.op-feature-prose :global(ul) {
|
|
906
|
+
.op-feature-prose :global(ul:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
756
907
|
list-style: disc;
|
|
757
908
|
}
|
|
758
909
|
|
|
759
|
-
.op-feature-prose :global(ol) {
|
|
910
|
+
.op-feature-prose :global(ol:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
760
911
|
list-style: decimal;
|
|
761
912
|
}
|
|
762
913
|
|
|
763
|
-
.op-feature-prose :global(li > :where(ul, ol)) {
|
|
914
|
+
.op-feature-prose :global(li > :where(ul, ol):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
764
915
|
margin: 0.25rem 0;
|
|
765
916
|
}
|
|
766
917
|
|
|
767
|
-
.op-feature-prose :global(li > p) {
|
|
918
|
+
.op-feature-prose :global(li > p:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
768
919
|
margin: 0;
|
|
769
920
|
}
|
|
770
921
|
|
|
771
|
-
.op-feature-prose :global(ul[data-type='taskList']) {
|
|
922
|
+
.op-feature-prose :global(ul[data-type='taskList']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
772
923
|
list-style: none;
|
|
773
924
|
padding-left: 0;
|
|
774
925
|
}
|
|
775
926
|
|
|
776
|
-
.op-feature-prose :global(li[data-type='taskItem']) {
|
|
927
|
+
.op-feature-prose :global(li[data-type='taskItem']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
777
928
|
display: flex;
|
|
778
929
|
align-items: flex-start;
|
|
779
930
|
gap: 0.5rem;
|
|
780
931
|
}
|
|
781
932
|
|
|
782
|
-
.op-feature-prose :global(li[data-type='taskItem'] > label) {
|
|
933
|
+
.op-feature-prose :global(li[data-type='taskItem'] > label:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
783
934
|
display: flex;
|
|
784
935
|
align-items: center;
|
|
785
936
|
min-height: 1.85em;
|
|
786
937
|
}
|
|
787
938
|
|
|
788
|
-
.op-feature-prose :global(li[data-type='taskItem'] > div > p) {
|
|
939
|
+
.op-feature-prose :global(li[data-type='taskItem'] > div > p:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
789
940
|
margin: 0;
|
|
790
941
|
}
|
|
791
942
|
|
|
792
|
-
.op-feature-prose :global(blockquote) {
|
|
943
|
+
.op-feature-prose :global(blockquote:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
793
944
|
border-left: 2px solid var(--orionplace-border);
|
|
794
945
|
padding-left: 1rem;
|
|
795
946
|
color: var(--orionplace-muted);
|
|
796
947
|
}
|
|
797
948
|
|
|
798
|
-
.op-feature-prose :global(hr) {
|
|
949
|
+
.op-feature-prose :global(hr:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
799
950
|
margin: 2rem 0;
|
|
800
951
|
border: 0;
|
|
801
952
|
border-top: 1px solid var(--orionplace-border);
|
|
802
953
|
}
|
|
803
954
|
|
|
804
|
-
.op-feature-prose :global(code) {
|
|
955
|
+
.op-feature-prose :global(code:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
805
956
|
padding: 0.1em 0.3em;
|
|
806
957
|
font-size: 0.875em;
|
|
807
958
|
background: var(--orionplace-muted-surface);
|
|
808
959
|
}
|
|
809
960
|
|
|
810
|
-
.op-feature-prose :global(pre) {
|
|
961
|
+
.op-feature-prose :global(pre:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
811
962
|
overflow-x: auto;
|
|
812
963
|
padding: 1rem;
|
|
813
964
|
line-height: 1.6;
|
|
@@ -815,13 +966,13 @@
|
|
|
815
966
|
background: var(--orionplace-muted-surface);
|
|
816
967
|
}
|
|
817
968
|
|
|
818
|
-
.op-feature-prose :global(pre code) {
|
|
969
|
+
.op-feature-prose :global(pre code:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
819
970
|
padding: 0;
|
|
820
971
|
background: transparent;
|
|
821
972
|
}
|
|
822
973
|
|
|
823
974
|
/* A wide table scrolls inside the article instead of widening the page. */
|
|
824
|
-
.op-feature-prose :global(table) {
|
|
975
|
+
.op-feature-prose :global(table:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
825
976
|
display: block;
|
|
826
977
|
max-width: 100%;
|
|
827
978
|
overflow-x: auto;
|
|
@@ -829,7 +980,7 @@
|
|
|
829
980
|
border-collapse: collapse;
|
|
830
981
|
}
|
|
831
982
|
|
|
832
|
-
.op-feature-prose :global(:where(th, td)) {
|
|
983
|
+
.op-feature-prose :global(:where(th, td):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
833
984
|
min-width: 6rem;
|
|
834
985
|
padding: 0.5rem 0.75rem;
|
|
835
986
|
vertical-align: top;
|
|
@@ -837,46 +988,46 @@
|
|
|
837
988
|
border: 1px solid var(--orionplace-border);
|
|
838
989
|
}
|
|
839
990
|
|
|
840
|
-
.op-feature-prose :global(th) {
|
|
991
|
+
.op-feature-prose :global(th:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
841
992
|
font-weight: 500;
|
|
842
993
|
background: var(--orionplace-muted-surface);
|
|
843
994
|
}
|
|
844
995
|
|
|
845
|
-
.op-feature-prose :global(caption) {
|
|
996
|
+
.op-feature-prose :global(caption:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
846
997
|
padding: 0.5rem 0;
|
|
847
998
|
color: var(--orionplace-muted);
|
|
848
999
|
text-align: left;
|
|
849
1000
|
}
|
|
850
1001
|
|
|
851
|
-
.op-feature-prose :global([data-text-align='center']) {
|
|
1002
|
+
.op-feature-prose :global([data-text-align='center']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
852
1003
|
text-align: center;
|
|
853
1004
|
}
|
|
854
1005
|
|
|
855
|
-
.op-feature-prose :global([data-text-align='right']) {
|
|
1006
|
+
.op-feature-prose :global([data-text-align='right']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
856
1007
|
text-align: right;
|
|
857
1008
|
}
|
|
858
1009
|
|
|
859
|
-
.op-feature-prose :global([data-vertical-align='middle']) {
|
|
1010
|
+
.op-feature-prose :global([data-vertical-align='middle']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
860
1011
|
vertical-align: middle;
|
|
861
1012
|
}
|
|
862
1013
|
|
|
863
|
-
.op-feature-prose :global([data-vertical-align='bottom']) {
|
|
1014
|
+
.op-feature-prose :global([data-vertical-align='bottom']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
864
1015
|
vertical-align: bottom;
|
|
865
1016
|
}
|
|
866
1017
|
|
|
867
1018
|
/* Images that open in the viewer. */
|
|
868
1019
|
[data-blog-article-cover][data-blog-image-viewer],
|
|
869
|
-
.op-feature-prose :global(img[data-blog-image-viewer]) {
|
|
1020
|
+
.op-feature-prose :global(img[data-blog-image-viewer]:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
870
1021
|
cursor: zoom-in;
|
|
871
1022
|
}
|
|
872
1023
|
|
|
873
1024
|
[data-blog-article-cover][data-blog-image-viewer]:focus-visible,
|
|
874
|
-
.op-feature-prose :global(img[data-blog-image-viewer]:focus-visible) {
|
|
1025
|
+
.op-feature-prose :global(img[data-blog-image-viewer]:focus-visible:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
875
1026
|
outline: 2px solid var(--orionplace-accent);
|
|
876
1027
|
outline-offset: 3px;
|
|
877
1028
|
}
|
|
878
1029
|
|
|
879
|
-
.op-feature-prose :global(:where(img, video)) {
|
|
1030
|
+
.op-feature-prose :global(:where(img, video):where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
880
1031
|
display: block;
|
|
881
1032
|
max-width: 100%;
|
|
882
1033
|
height: auto;
|
|
@@ -886,31 +1037,31 @@
|
|
|
886
1037
|
/* A narrow article has no room for text beside a floated image; 592px is
|
|
887
1038
|
the article's width in the /journal page on a 640px screen. */
|
|
888
1039
|
@container article (max-width: 591px) {
|
|
889
|
-
.op-feature-prose :global(:where(img, video)[style*='float']) {
|
|
1040
|
+
.op-feature-prose :global(:where(img, video)[style*='float']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
890
1041
|
float: none !important;
|
|
891
1042
|
margin: 1rem auto !important;
|
|
892
1043
|
}
|
|
893
1044
|
}
|
|
894
1045
|
|
|
895
|
-
.op-feature-prose :global(audio) {
|
|
1046
|
+
.op-feature-prose :global(audio:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
896
1047
|
display: block;
|
|
897
1048
|
width: 100%;
|
|
898
1049
|
}
|
|
899
1050
|
|
|
900
|
-
.op-feature-prose :global(figcaption) {
|
|
1051
|
+
.op-feature-prose :global(figcaption:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
901
1052
|
margin-top: 0.5rem;
|
|
902
1053
|
font-size: 0.875rem;
|
|
903
1054
|
color: var(--orionplace-muted);
|
|
904
1055
|
}
|
|
905
1056
|
|
|
906
|
-
.op-feature-prose :global(.blog-image-gallery__track) {
|
|
1057
|
+
.op-feature-prose :global(.blog-image-gallery__track:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
907
1058
|
display: flex;
|
|
908
1059
|
gap: 0.5rem;
|
|
909
1060
|
overflow-x: auto;
|
|
910
1061
|
scroll-snap-type: x mandatory;
|
|
911
1062
|
}
|
|
912
1063
|
|
|
913
|
-
.op-feature-prose :global(.blog-image-gallery__track img) {
|
|
1064
|
+
.op-feature-prose :global(.blog-image-gallery__track img:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
914
1065
|
flex: 0 0 auto;
|
|
915
1066
|
width: auto;
|
|
916
1067
|
max-width: min(80%, 32rem);
|
|
@@ -919,105 +1070,123 @@
|
|
|
919
1070
|
scroll-snap-align: start;
|
|
920
1071
|
}
|
|
921
1072
|
|
|
922
|
-
.op-feature-prose :global(details) {
|
|
1073
|
+
.op-feature-prose :global(details:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
923
1074
|
margin: 1rem 0;
|
|
924
1075
|
padding: 0.75rem 1rem;
|
|
925
1076
|
border: 1px solid var(--orionplace-border);
|
|
926
1077
|
}
|
|
927
1078
|
|
|
928
|
-
.op-feature-prose :global(summary) {
|
|
1079
|
+
.op-feature-prose :global(summary:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
929
1080
|
cursor: pointer;
|
|
930
1081
|
}
|
|
931
1082
|
|
|
932
|
-
.op-feature-prose :global([data-type='math-inline']),
|
|
933
|
-
.op-feature-prose :global([data-type='math-block']) {
|
|
1083
|
+
.op-feature-prose :global([data-type='math-inline']:where(:not([data-product-id] *, [data-product-ids] *))),
|
|
1084
|
+
.op-feature-prose :global([data-type='math-block']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
934
1085
|
font-family: ui-monospace, monospace;
|
|
935
1086
|
font-size: 0.9em;
|
|
936
1087
|
}
|
|
937
1088
|
|
|
938
|
-
.op-feature-prose :global([data-type='math-block']) {
|
|
1089
|
+
.op-feature-prose :global([data-type='math-block']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
939
1090
|
display: block;
|
|
940
1091
|
margin: 1rem 0;
|
|
941
1092
|
overflow-x: auto;
|
|
942
1093
|
text-align: center;
|
|
943
1094
|
}
|
|
944
1095
|
|
|
945
|
-
/* Product placements
|
|
1096
|
+
/* Product placements: cards fill them; one without its products stays hidden. */
|
|
946
1097
|
.op-feature-prose
|
|
947
1098
|
:global(:where([data-product-id], [data-product-ids]):empty) {
|
|
948
1099
|
display: none;
|
|
949
1100
|
}
|
|
950
1101
|
|
|
1102
|
+
.op-feature-prose :global([data-product-ids]) {
|
|
1103
|
+
clear: both;
|
|
1104
|
+
margin: 1.5rem 0;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.op-feature-prose :global([data-product-id]:not([data-product-ids])) {
|
|
1108
|
+
width: min(100%, 260px);
|
|
1109
|
+
margin-block: 1.5rem;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/* The editor's float has no room beside a card on a narrow page. */
|
|
1113
|
+
@container article (max-width: 480px) {
|
|
1114
|
+
.op-feature-prose :global([data-product-id]:not([data-product-ids])) {
|
|
1115
|
+
float: none !important;
|
|
1116
|
+
margin-inline: auto !important;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
951
1120
|
/* The editor's named colors, mixed with the theme text for contrast. */
|
|
952
|
-
.op-feature-prose :global([data-text-color='gray']) {
|
|
1121
|
+
.op-feature-prose :global([data-text-color='gray']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
953
1122
|
color: color-mix(in srgb, gray 70%, var(--orionplace-text));
|
|
954
1123
|
}
|
|
955
1124
|
|
|
956
|
-
.op-feature-prose :global([data-bg-color='gray']) {
|
|
1125
|
+
.op-feature-prose :global([data-bg-color='gray']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
957
1126
|
background: color-mix(in srgb, gray 18%, transparent);
|
|
958
1127
|
}
|
|
959
1128
|
|
|
960
|
-
.op-feature-prose :global([data-text-color='brown']) {
|
|
1129
|
+
.op-feature-prose :global([data-text-color='brown']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
961
1130
|
color: color-mix(in srgb, brown 70%, var(--orionplace-text));
|
|
962
1131
|
}
|
|
963
1132
|
|
|
964
|
-
.op-feature-prose :global([data-bg-color='brown']) {
|
|
1133
|
+
.op-feature-prose :global([data-bg-color='brown']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
965
1134
|
background: color-mix(in srgb, brown 18%, transparent);
|
|
966
1135
|
}
|
|
967
1136
|
|
|
968
|
-
.op-feature-prose :global([data-text-color='orange']) {
|
|
1137
|
+
.op-feature-prose :global([data-text-color='orange']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
969
1138
|
color: color-mix(in srgb, orange 70%, var(--orionplace-text));
|
|
970
1139
|
}
|
|
971
1140
|
|
|
972
|
-
.op-feature-prose :global([data-bg-color='orange']) {
|
|
1141
|
+
.op-feature-prose :global([data-bg-color='orange']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
973
1142
|
background: color-mix(in srgb, orange 18%, transparent);
|
|
974
1143
|
}
|
|
975
1144
|
|
|
976
|
-
.op-feature-prose :global([data-text-color='yellow']) {
|
|
1145
|
+
.op-feature-prose :global([data-text-color='yellow']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
977
1146
|
color: color-mix(in srgb, yellow 70%, var(--orionplace-text));
|
|
978
1147
|
}
|
|
979
1148
|
|
|
980
|
-
.op-feature-prose :global([data-bg-color='yellow']) {
|
|
1149
|
+
.op-feature-prose :global([data-bg-color='yellow']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
981
1150
|
background: color-mix(in srgb, yellow 18%, transparent);
|
|
982
1151
|
}
|
|
983
1152
|
|
|
984
|
-
.op-feature-prose :global([data-text-color='green']) {
|
|
1153
|
+
.op-feature-prose :global([data-text-color='green']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
985
1154
|
color: color-mix(in srgb, green 70%, var(--orionplace-text));
|
|
986
1155
|
}
|
|
987
1156
|
|
|
988
|
-
.op-feature-prose :global([data-bg-color='green']) {
|
|
1157
|
+
.op-feature-prose :global([data-bg-color='green']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
989
1158
|
background: color-mix(in srgb, green 18%, transparent);
|
|
990
1159
|
}
|
|
991
1160
|
|
|
992
|
-
.op-feature-prose :global([data-text-color='blue']) {
|
|
1161
|
+
.op-feature-prose :global([data-text-color='blue']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
993
1162
|
color: color-mix(in srgb, blue 70%, var(--orionplace-text));
|
|
994
1163
|
}
|
|
995
1164
|
|
|
996
|
-
.op-feature-prose :global([data-bg-color='blue']) {
|
|
1165
|
+
.op-feature-prose :global([data-bg-color='blue']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
997
1166
|
background: color-mix(in srgb, blue 18%, transparent);
|
|
998
1167
|
}
|
|
999
1168
|
|
|
1000
|
-
.op-feature-prose :global([data-text-color='purple']) {
|
|
1169
|
+
.op-feature-prose :global([data-text-color='purple']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1001
1170
|
color: color-mix(in srgb, purple 70%, var(--orionplace-text));
|
|
1002
1171
|
}
|
|
1003
1172
|
|
|
1004
|
-
.op-feature-prose :global([data-bg-color='purple']) {
|
|
1173
|
+
.op-feature-prose :global([data-bg-color='purple']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1005
1174
|
background: color-mix(in srgb, purple 18%, transparent);
|
|
1006
1175
|
}
|
|
1007
1176
|
|
|
1008
|
-
.op-feature-prose :global([data-text-color='pink']) {
|
|
1177
|
+
.op-feature-prose :global([data-text-color='pink']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1009
1178
|
color: color-mix(in srgb, pink 70%, var(--orionplace-text));
|
|
1010
1179
|
}
|
|
1011
1180
|
|
|
1012
|
-
.op-feature-prose :global([data-bg-color='pink']) {
|
|
1181
|
+
.op-feature-prose :global([data-bg-color='pink']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1013
1182
|
background: color-mix(in srgb, pink 18%, transparent);
|
|
1014
1183
|
}
|
|
1015
1184
|
|
|
1016
|
-
.op-feature-prose :global([data-text-color='red']) {
|
|
1185
|
+
.op-feature-prose :global([data-text-color='red']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1017
1186
|
color: color-mix(in srgb, red 70%, var(--orionplace-text));
|
|
1018
1187
|
}
|
|
1019
1188
|
|
|
1020
|
-
.op-feature-prose :global([data-bg-color='red']) {
|
|
1189
|
+
.op-feature-prose :global([data-bg-color='red']:where(:not([data-product-id] *, [data-product-ids] *))) {
|
|
1021
1190
|
background: color-mix(in srgb, red 18%, transparent);
|
|
1022
1191
|
}
|
|
1023
1192
|
</style>
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
actionsDisabled?: boolean;
|
|
53
53
|
purchaseActions?: boolean;
|
|
54
54
|
variantProductIds?: number[];
|
|
55
|
+
// Badge keys not to show on this card (new / popular / deal).
|
|
56
|
+
hiddenBadges?: string[];
|
|
55
57
|
onSelect?: (...args: [product: IProduct]) => unknown;
|
|
56
58
|
onSelectSeller?: (...args: [seller: IProductSeller]) => unknown;
|
|
57
59
|
onToggleFavorite?: (
|
|
@@ -88,6 +90,7 @@
|
|
|
88
90
|
actionsDisabled: input_actionsDisabled = false,
|
|
89
91
|
purchaseActions: input_purchaseActions = true,
|
|
90
92
|
variantProductIds: input_variantProductIds,
|
|
93
|
+
hiddenBadges: input_hiddenBadges = [],
|
|
91
94
|
onSelect: callback_onSelect,
|
|
92
95
|
onSelectSeller: callback_onSelectSeller,
|
|
93
96
|
onToggleFavorite: callback_onToggleFavorite,
|
|
@@ -128,6 +131,9 @@
|
|
|
128
131
|
get variantProductIds() {
|
|
129
132
|
return input_variantProductIds;
|
|
130
133
|
},
|
|
134
|
+
get hiddenBadges() {
|
|
135
|
+
return input_hiddenBadges;
|
|
136
|
+
},
|
|
131
137
|
get footer() {
|
|
132
138
|
return snippet_footer;
|
|
133
139
|
},
|
|
@@ -155,6 +161,11 @@
|
|
|
155
161
|
const card = ref<HTMLElement>();
|
|
156
162
|
const { minHeight } = useOrionplaceStableHeight(card);
|
|
157
163
|
|
|
164
|
+
// An empty label hides the badge; undefined keeps the locale's label.
|
|
165
|
+
function hiddenBadgeLabel(key: string) {
|
|
166
|
+
return inputs.hiddenBadges.includes(key) ? '' : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
const store = useOrionplaceStore();
|
|
159
170
|
const productStore = useOrionplaceProductsStore();
|
|
160
171
|
const selectionStore = useOrionplaceSelectionsStore();
|
|
@@ -664,6 +675,9 @@
|
|
|
664
675
|
productId={displayedProductId.value}
|
|
665
676
|
collection={inputs.collection}
|
|
666
677
|
lowStockLabel=""
|
|
678
|
+
newLabel={hiddenBadgeLabel('new')}
|
|
679
|
+
popularLabel={hiddenBadgeLabel('popular')}
|
|
680
|
+
dealLabel={hiddenBadgeLabel('deal')}
|
|
667
681
|
class={['pointer-events-none absolute']}
|
|
668
682
|
/>
|
|
669
683
|
{/if}
|
|
@@ -789,6 +803,9 @@
|
|
|
789
803
|
productId={displayedProductId.value}
|
|
790
804
|
collection={inputs.collection}
|
|
791
805
|
lowStockLabel=""
|
|
806
|
+
newLabel={hiddenBadgeLabel('new')}
|
|
807
|
+
popularLabel={hiddenBadgeLabel('popular')}
|
|
808
|
+
dealLabel={hiddenBadgeLabel('deal')}
|
|
792
809
|
/>
|
|
793
810
|
{/if}
|
|
794
811
|
<ProductCardPrice
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
limit?: number;
|
|
28
28
|
productIds?: number[];
|
|
29
29
|
layout?: 'responsive' | 'grid';
|
|
30
|
+
hiddenBadges?: string[];
|
|
30
31
|
onSelect?: (...args: [product: IProduct]) => unknown;
|
|
31
32
|
onSelectSeller?: (...args: [seller: IProductSeller]) => unknown;
|
|
32
33
|
onRequestSecondaryVariants?: (
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
limit: input_limit = 0,
|
|
60
61
|
productIds: input_productIds,
|
|
61
62
|
layout: input_layout = 'responsive',
|
|
63
|
+
hiddenBadges: input_hiddenBadges = [],
|
|
62
64
|
onSelect: callback_onSelect,
|
|
63
65
|
onSelectSeller: callback_onSelectSeller,
|
|
64
66
|
onRequestSecondaryVariants: callback_onRequestSecondaryVariants,
|
|
@@ -89,6 +91,9 @@
|
|
|
89
91
|
get layout() {
|
|
90
92
|
return input_layout;
|
|
91
93
|
},
|
|
94
|
+
get hiddenBadges() {
|
|
95
|
+
return input_hiddenBadges;
|
|
96
|
+
},
|
|
92
97
|
get footer() {
|
|
93
98
|
return snippet_footer;
|
|
94
99
|
},
|
|
@@ -207,6 +212,7 @@
|
|
|
207
212
|
productId={product.id}
|
|
208
213
|
collection={inputs.collection}
|
|
209
214
|
disabled={inputs.disabled}
|
|
215
|
+
hiddenBadges={inputs.hiddenBadges}
|
|
210
216
|
onSelect={($event) => {
|
|
211
217
|
emit('select', $event);
|
|
212
218
|
}}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Product placements in article text. The marketplace blog editor writes an
|
|
3
|
+
* empty block where the author put products:
|
|
4
|
+
* <div data-product-id="12"></div> one product card
|
|
5
|
+
* <div data-product-ids="12,15,9"></div> a row of cards (up to 24)
|
|
6
|
+
* with optional data-product-hidden-badges="new,popular,good-deal" and a
|
|
7
|
+
* float or centered style. The article shows product cards in those blocks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const blogPlacementMaxProducts = 24;
|
|
11
|
+
|
|
12
|
+
// The backend lists at most 35 products per page.
|
|
13
|
+
export const blogPlacementPageSize = 35;
|
|
14
|
+
|
|
15
|
+
export const blogPlacementSelector = '[data-product-id], [data-product-ids]';
|
|
16
|
+
|
|
17
|
+
// Editor badge names → product card badge keys.
|
|
18
|
+
const placementBadges = {
|
|
19
|
+
'new': 'new',
|
|
20
|
+
'popular': 'popular',
|
|
21
|
+
'good-deal': 'deal',
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type TBlogPlacementBadge = (typeof placementBadges)[keyof typeof placementBadges];
|
|
25
|
+
|
|
26
|
+
export interface IBlogProductPlacement {
|
|
27
|
+
element: HTMLElement;
|
|
28
|
+
layout: 'single' | 'list';
|
|
29
|
+
productIds: number[];
|
|
30
|
+
hiddenBadges: TBlogPlacementBadge[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Positive whole ids from «12,15,9», without repeats, at most `limit`. */
|
|
34
|
+
export function blogPlacementIds(value: string | null | undefined, limit = blogPlacementMaxProducts): number[] {
|
|
35
|
+
const ids: number[] = [];
|
|
36
|
+
|
|
37
|
+
if (!value || value.length > 512) {
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
for (const part of value.split(',')) {
|
|
41
|
+
const source = part.trim();
|
|
42
|
+
const id = /^\d+$/.test(source) ? Number(source) : 0;
|
|
43
|
+
|
|
44
|
+
if (Number.isSafeInteger(id) && id > 0 && !ids.includes(id)) {
|
|
45
|
+
ids.push(id);
|
|
46
|
+
}
|
|
47
|
+
if (ids.length >= limit) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Card badge keys the author turned off for this placement. */
|
|
55
|
+
export function blogPlacementHiddenBadges(value: string | null | undefined): TBlogPlacementBadge[] {
|
|
56
|
+
if (!value || value.length > 512) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const names = value.split(',').map((name) => name.trim().toLowerCase());
|
|
60
|
+
|
|
61
|
+
return Object.entries(placementBadges)
|
|
62
|
+
.filter(([name]) => names.includes(name))
|
|
63
|
+
.map(([, key]) => key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The placements of a rendered article, in text order. */
|
|
67
|
+
export function blogProductPlacements(root: ParentNode | null | undefined): IBlogProductPlacement[] {
|
|
68
|
+
if (typeof root?.querySelectorAll !== 'function') {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return Array.from(root.querySelectorAll<HTMLElement>(blogPlacementSelector)).flatMap((element) => {
|
|
72
|
+
// Product cards inside a filled placement carry data-product-id too.
|
|
73
|
+
if (element.parentElement?.closest(blogPlacementSelector)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const list = element.hasAttribute('data-product-ids');
|
|
77
|
+
const productIds = list
|
|
78
|
+
? blogPlacementIds(element.getAttribute('data-product-ids'))
|
|
79
|
+
: blogPlacementIds(element.getAttribute('data-product-id'), 1);
|
|
80
|
+
|
|
81
|
+
return productIds.length
|
|
82
|
+
? [{
|
|
83
|
+
element,
|
|
84
|
+
layout: list ? 'list' : 'single',
|
|
85
|
+
productIds,
|
|
86
|
+
hiddenBadges: blogPlacementHiddenBadges(element.getAttribute('data-product-hidden-badges')),
|
|
87
|
+
}]
|
|
88
|
+
: [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every product id of the placements, once, in text order. */
|
|
93
|
+
export function blogPlacementProductIds(placements: Pick<IBlogProductPlacement, 'productIds'>[]): number[] {
|
|
94
|
+
return [...new Set(placements.flatMap((placement) => placement.productIds))];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Ids split into requests the backend answers in one page each. */
|
|
98
|
+
export function blogPlacementRequests(ids: number[]): number[][] {
|
|
99
|
+
const requests: number[][] = [];
|
|
100
|
+
|
|
101
|
+
for (let start = 0; start < ids.length; start += blogPlacementPageSize) {
|
|
102
|
+
requests.push(ids.slice(start, start + blogPlacementPageSize));
|
|
103
|
+
}
|
|
104
|
+
return requests;
|
|
105
|
+
}
|