Skip to content

Commit 8514d73

Browse files
[Recommendation] Ajaxify recommendation
1 parent 8fe1b3f commit 8514d73

6 files changed

Lines changed: 115 additions & 21 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
import '@elastic-suite/gally-sdk/browser';
22
import '../../public/filters.css';
3+
import '../../public/recommendations.css';

src/Resources/config/services/twig/component/product.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
<tag name="sylius.twig_component" key="gally_shop:product:sorting"/>
1919
</service>
2020

21-
<service id="Gally\SyliusPlugin\Twig\Component\Product\RecommendationComponent">
21+
<service id="Gally\SyliusPlugin\Twig\Component\Product\RecommendationComponent" autoconfigure="true">
2222
<argument type="service" id="sylius.context.channel" />
2323
<argument type="service" id="Gally\SyliusPlugin\Config\ConfigManager" />
2424
<argument type="service" id="Gally\SyliusPlugin\Recommendation\RecommendationFinder" />
25-
<tag name="sylius.twig_component" key="gally_shop:product:recommendation"/>
25+
<argument type="service" id="sylius.repository.product_association_type" />
26+
<argument type="service" id="sylius.repository.product" />
2627
</service>
2728
</services>
2829
</container>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.product-card-placeholder-line.title {
2+
width: 12rem;
3+
height: 1.5rem;
4+
margin-bottom: 1.25rem;
5+
}
6+
7+
.products-grid-placeholder {
8+
display: grid;
9+
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
10+
grid-gap: 3rem 2rem;
11+
}
12+
13+
.product-card-placeholder {
14+
display: flex;
15+
flex-direction: column;
16+
gap: 0.75rem;
17+
}
18+
19+
.product-card-placeholder-image {
20+
aspect-ratio: 1 / 1;
21+
border-radius: 0.25rem;
22+
}
23+
24+
.product-card-placeholder-line {
25+
height: 0.9rem;
26+
border-radius: 0.25rem;
27+
}
28+
29+
.product-card-placeholder-line.short {
30+
width: 40%;
31+
}
32+
33+
.product-card-placeholder-image,
34+
.product-card-placeholder-line {
35+
background: var(--bs-secondary-bg-subtle, #e9ecef);
36+
animation: gally-placeholder-pulse 1.4s ease-in-out infinite;
37+
}
38+
39+
@keyframes gally-placeholder-pulse {
40+
0%, 100%
41+
{
42+
opacity: 1;
43+
}
44+
50%
45+
{
46+
opacity: 0.4;
47+
}
48+
}
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1-
{% if associated_products is not empty %}
2-
<h2 class="mb-5">{{ product_association_type.name }}</h2>
3-
<div {{ sylius_test_html_attribute('product-recommendation', product_association_type.name) }}>
4-
<div class="products-grid">
5-
{% for product in associated_products %}
6-
<div>{{ component('sylius_shop:product:card', { product: product, template: '@SyliusShop/product/common/card.html.twig' }) }}</div>
7-
{% endfor %}
1+
<div{{ attributes }}>
2+
{% if associated_products is not empty %}
3+
<h2 class="mb-5">{{ product_association_type.name }}</h2>
4+
<div {{ sylius_test_html_attribute('product-recommendation', product_association_type.name) }}>
5+
<div class="products-grid">
6+
{% for product in associated_products %}
7+
<div>{{ component('sylius_shop:product:card', { product: product, template: '@SyliusShop/product/common/card.html.twig' }) }}</div>
8+
{% endfor %}
9+
</div>
810
</div>
11+
{% else %}
12+
{{ component('sylius_shop:product:list', { limit: 4, template: '@SyliusShop/product/common/list.html.twig' }) }}
13+
{% endif %}
14+
</div>
15+
16+
{% macro placeholder(props) %}
17+
<div class="product-card-placeholder-line title"></div>
18+
<div class="products-grid-placeholder">
19+
{% for _ in 1..4 %}
20+
<div class="product-card-placeholder">
21+
<div class="product-card-placeholder-image"></div>
22+
<div class="product-card-placeholder-line"></div>
23+
<div class="product-card-placeholder-line short"></div>
24+
</div>
25+
{% endfor %}
926
</div>
10-
{% endif %}
27+
{% endmacro %}

src/Resources/views/shop/product/show/content/product_listing/recommendations.html.twig

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,11 @@
44
{% set cart_recommendation_type_code = gally.getCartRecommendationTypeCode() %}
55
{% set association_types = gally.getProductAssociationTypes()|filter(type => type.code != cart_recommendation_type_code) %}
66

7-
{% set rendered_blocks %}
8-
{% for association_type in association_types %}
9-
{{ component('gally_shop:product:recommendation', { productAssociationType: association_type, product: product, template: '@GallySyliusPlugin/shop/product/show/content/product_listing/recommendation.html.twig' }) }}
10-
{% endfor %}
11-
{% endset %}
12-
13-
{% if rendered_blocks|trim is not empty %}
14-
{{ rendered_blocks|raw }}
7+
{% for association_type in association_types %}
8+
{{ component('gally_shop:product:recommendation', { productAssociationType: association_type, product: product, loading: 'lazy' }) }}
159
{% else %}
1610
{{ component('sylius_shop:product:list', { limit: 4, template: '@SyliusShop/product/common/list.html.twig' }) }}
17-
{% endif %}
11+
{% endfor %}
1812
{% else %}
1913
{% for association in product.associations %}
2014
{{ component('sylius_shop:product:association', { productAssociation: association, template: '@SyliusShop/product/show/content/product_listing/association.html.twig' }) }}

src/Twig/Component/Product/RecommendationComponent.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,66 @@
2020
use Sylius\Component\Channel\Context\ChannelContextInterface;
2121
use Sylius\Component\Product\Model\ProductAssociationTypeInterface;
2222
use Sylius\Component\Product\Model\ProductInterface;
23+
use Sylius\Component\Resource\Repository\RepositoryInterface;
2324
use Sylius\TwigHooks\Twig\Component\HookableComponentTrait;
24-
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
25+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
26+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
27+
use Symfony\UX\LiveComponent\DefaultActionTrait;
2528
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
2629

2730
/**
2831
* Displays, for a given product association type, the manually curated ("hard") associated products
2932
* first, then the Gally-recommended products for the same type. Unlike Sylius' native association
3033
* component, it is mounted with the type itself (not an existing ProductAssociation row), so it also
3134
* renders Gally-only recommendations for products that have no "hard" association of that type yet.
35+
*
36+
* Rendered with `loading: 'lazy'` from recommendations.html.twig so the Gally call doesn't block the
37+
* product page's initial render.
3238
*/
33-
#[AsTwigComponent]
39+
#[AsLiveComponent(name: 'gally_shop:product:recommendation', template: '@GallySyliusPlugin/shop/product/show/content/product_listing/recommendation.html.twig')]
3440
class RecommendationComponent
3541
{
42+
use DefaultActionTrait;
3643
use HookableComponentTrait;
3744

45+
#[LiveProp(hydrateWith: 'hydrateProductAssociationType', dehydrateWith: 'dehydrateProductAssociationType')]
3846
#[ExposeInTemplate('product_association_type')]
3947
public ProductAssociationTypeInterface $productAssociationType;
4048

49+
#[LiveProp(hydrateWith: 'hydrateProduct', dehydrateWith: 'dehydrateProduct')]
4150
public ProductInterface $product;
4251

4352
public function __construct(
4453
private ChannelContextInterface $channelContext,
4554
private ConfigManager $configManager,
4655
private RecommendationFinder $recommendationFinder,
56+
private RepositoryInterface $productAssociationTypeRepository,
57+
private RepositoryInterface $productRepository,
4758
) {
4859
}
4960

61+
public function dehydrateProductAssociationType(ProductAssociationTypeInterface $productAssociationType): string
62+
{
63+
return (string) $productAssociationType->getCode();
64+
}
65+
66+
public function hydrateProductAssociationType(string $code): ProductAssociationTypeInterface
67+
{
68+
/** @var ProductAssociationTypeInterface $productAssociationType */
69+
return $this->productAssociationTypeRepository->findOneBy(['code' => $code]);
70+
}
71+
72+
public function dehydrateProduct(ProductInterface $product): string
73+
{
74+
return (string) $product->getCode();
75+
}
76+
77+
public function hydrateProduct(string $code): ProductInterface
78+
{
79+
/** @var ProductInterface $product */
80+
return $this->productRepository->findOneBy(['code' => $code]);
81+
}
82+
5083
/**
5184
* @return ProductInterface[]
5285
*/

0 commit comments

Comments
 (0)