This file is a merged representation of a subset of the codebase, containing files not matching ignore patterns, combined into a single document by Repomix.

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching these patterns are excluded: ai_docs/done
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded

Additional Info:
----------------

================================================================
Directory Structure
================================================================
manual/
  index.de.md
  index.en.md
  topfeed-configuration.de.md
  topfeed-configuration.en.md
src/
  Component/
    Collection.php
  Content/
    Product/
      Events/
        AssociatedProductsCriteriaEvent.php
  Controller/
    TopFeedApiController.php
  Resources/
    app/
      administration/
        src/
          module/
            sw-category/
              component/
                sw-category-view/
                  index.js
                  sw-category-view.html.twig
          snippet/
            de-DE.json
            en-GB.json
          view/
            category-detail-topfeed/
              category-detail-topfeed.html.twig
              category-detail-topfeed.scss
              index.js
          main.js
      storefront/
        src/
          page/
            topdata-top-feed.linked-products-list.plugin.js
            topdata-top-feed.linked-products.plugin.js
            topdata-top-feed.product-detail-configurator-select.plugin.js
            topdata-top-feed.product-detail-tab.plugin.js
          scss/
            base.scss
          main.js
    config/
      config.xml
      routes.xml
      services.xml
    snippet/
      de_DE/
        SnippetFile_de_DE.php
        storefront.de-DE.json
      en_GB/
        SnippetFile_en_GB.php
        storefront.en-GB.json
    views/
      storefront/
        component/
          product/
            card/
              box-standard.html.twig
        element/
          cms-element-product-description-reviews.html.twig
        layout/
          header/
            header.html.twig
        page/
          linked-products-popup/
            index.html.twig
          list-linked-products/
            all-variants.html.twig
            index.html.twig
            product.html.twig
          product-detail/
            buy-widget.html.twig
            feed-devices-content.html.twig
            feed-devices-list-type.html.twig
            feed-devices-list.html.twig
            feed-devices-tab.html.twig
            products-tab.html.twig
            variants.html.twig
  Service/
    LinkedProductService.php
    PageExtensionService.php
    ProductPageExtender.php
    SettingsService.php
    TopfeedHelperServiceV1.php
    TopfeedHelperServiceV2.php
  Storefront/
    Page/
      CompatibleDevicesWidget/
        CompatibleDevicesWidgetPageV1.php
        CompatibleDevicesWidgetPageV1Loader.php
      LinkedProductsPopup/
        LinkedProductsPopupLoadedEvent.php
        LinkedProductsPopupLoader.php
        LinkedProductsPopupPage.php
        LinkTypeEnum.php
        ProductIdRetriever.php
  Subscriber/
    ProductMenuLoadedEventSubscriber.php
    ProductPageCriteriaSubscriber.php
    ProductPageLoadedEventSubscriber.php
  TopdataTopFeedSW6.php
.gitignore
.php-cs-fixer.dist.php
composer.json
package.json
php-cs-fixer.md
README.md
rector.md
rector.phar
rector.php
vite.config.js

================================================================
Files
================================================================

================
File: manual/index.de.md
================
# TopFEED 

...

================
File: manual/index.en.md
================
# TopFEED Configuration Guide

...

================
File: manual/topfeed-configuration.de.md
================
# TopFEED Konfigurationsleitfaden

## Übersicht
TopFEED ist ein Shopware-Plugin zur Verwaltung und Anzeige von Produktdaten. Diese Dokumentation beschreibt die verfügbaren Konfigurationsoptionen.

## Plugin-Information
- **Version**: 1.0.1
- **Kompatibilität**: Shopware 6.3.0.0 - 6.5.5.2
- **Support**: shopware@topdata.de

## Features
* Hochwertige Produktdaten aus eigener Pflege
* Cross & Upselling
* Artikeleigenschaften & Spezifikationen

## Konfigurationsoptionen

### Grundeinstellungen
1. **Verkaufskanal**
   - Wählen Sie den Verkaufskanal, für den die Einstellungen gelten sollen

### Produktdaten-Import
2. **Produktname importieren**
   - Legen Sie fest, ob der von uns definierte Produktname oder Ihr eigener verwendet werden soll

3. **Produktbeschreibung importieren**
   - Legen Sie fest, ob die von uns definierte Produktbeschreibung oder Ihre eigene verwendet werden soll

4. **Produkthersteller**
   - Legen Sie fest, ob der von uns definierte Produkthersteller oder Ihr eigener verwendet werden soll

5. **EANs importieren**
   - Legen Sie fest, ob die EAN-Nummer des Artikels importiert werden soll

6. **OEMs importieren**
   - Legen Sie fest, ob die OEM-Nummer des Artikels importiert werden soll

### Produktbilder
7. **Produktbilder**
   - Legen Sie fest, ob unsere Produktbilder oder Ihre eigenen verwendet werden sollen

8. **Alte Bilder löschen**
   - Legen Sie fest, ob nicht mehr aktuelle Produktbilder gelöscht werden sollen

### Produkteigenschaften
9. **Produkteigenschaften**
   - Legen Sie fest, ob von uns definierte Spezifikationen importiert werden sollen
   - Beispiele: Kapazität, Farbe, Produktart, etc.

10. **Referenz-PCDs als Merkmale**
    - Legen Sie fest, ob Referenz-PCDs als Eigenschaften gespeichert werden sollen

11. **Referenz-OEMs als Merkmale**
    - Legen Sie fest, ob Referenz-OEMs als Eigenschaften gespeichert werden sollen

### Kategorien
12. **Produktkategorien**
    - Legen Sie fest, ob Sie von uns angelegte Warengruppen oder Ihre eigenen verwenden möchten

13. **Hauptkategorie**
    - Legen Sie die Hauptwarengruppe für die Produktwarengruppen fest

### Cross-Selling Optionen
14. **Ähnliche Produkte**
    - Import ähnlicher Produkte aktivieren/deaktivieren

15. **Alternative Produkte**
    - Import alternativer Produkte aktivieren/deaktivieren

16. **Verwandte Produkte**
    - Import von Zubehör aktivieren/deaktivieren

17. **Gebündelte Produkte**
    - Import von Produktbundles aktivieren/deaktivieren

18. **Varianten**
    - Import von Produktvarianten aktivieren/deaktivieren

19. **Farbvarianten**
    - Import von Farbvarianten aktivieren/deaktivieren

20. **Kapazitätsvarianten**
    - Import von Kapazitätsvarianten aktivieren/deaktivieren

### Anzeigeeinstellungen

#### Produktdetailseite
21. **Andere Farben - Anzeige**
    - Konfiguration der Anzeige von Farbvarianten

22. **Andere Kapazitäten - Anzeige**
    - Konfiguration der Anzeige von Kapazitätsvarianten

23. **Produktvarianten**
    - Anzeige von Produktvarianten aktivieren/deaktivieren

24. **Andere Farben**
    - Anzeige von Farbvarianten aktivieren/deaktivieren

25. **Andere Kapazitäten**
    - Anzeige von Kapazitätsvarianten aktivieren/deaktivieren

26. **Alternative Produkte**
    - Anzeige alternativer Produkte aktivieren/deaktivieren

27. **Produkte im Bundle**
    - Anzeige von Bundleprodukten aktivieren/deaktivieren

28. **Bündel mit Produkt**
    - Anzeige von Bundles, die das Produkt enthalten, aktivieren/deaktivieren

29. **Zubehör**
    - Anzeige von Zubehör aktivieren/deaktivieren

30. **Ähnliche Produkte**
    - Anzeige ähnlicher Produkte aktivieren/deaktivieren

#### Produkt-Menü
31. **Produktvarianten**
    - Anzeige von Varianten im Menü aktivieren/deaktivieren

32. **Andere Farben**
    - Anzeige von Farbvarianten im Menü aktivieren/deaktivieren

33. **Andere Kapazitäten**
    - Anzeige von Kapazitätsvarianten im Menü aktivieren/deaktivieren

34. **Alternative Produkte**
    - Anzeige alternativer Produkte im Menü aktivieren/deaktivieren

35. **Produkte im Bundle**
    - Anzeige von Bundleprodukten im Menü aktivieren/deaktivieren

36. **Bündel mit Produkt**
    - Anzeige von Bundles im Menü aktivieren/deaktivieren

37. **Zubehör**
    - Anzeige von Zubehör im Menü aktivieren/deaktivieren

38. **Ähnliche Produkte**
    - Anzeige ähnlicher Produkte im Menü aktivieren/deaktivieren

#### Produktliste
39. **Produktvarianten**
    - Anzeige von Varianten in der Liste aktivieren/deaktivieren

40. **Andere Farben**
    - Anzeige von Farbvarianten in der Liste aktivieren/deaktivieren

41. **Andere Kapazitäten**
    - Anzeige von Kapazitätsvarianten in der Liste aktivieren/deaktivieren

42. **Alternative Produkte**
    - Anzeige alternativer Produkte in der Liste aktivieren/deaktivieren

43. **Produkte im Bundle**
    - Anzeige von Bundleprodukten in der Liste aktivieren/deaktivieren

44. **Bündel mit Produkt**
    - Anzeige von Bundles in der Liste aktivieren/deaktivieren

45. **Zubehör**
    - Anzeige von Zubehör in der Liste aktivieren/deaktivieren

46. **Ähnliche Produkte**
    - Anzeige ähnlicher Produkte in der Liste aktivieren/deaktivieren

================
File: manual/topfeed-configuration.en.md
================
# TopFEED Configuration Guide

## Overview
TopFEED is a Shopware plugin for managing and displaying product data. This documentation describes the available configuration options.

## Plugin Information
- **Version**: 1.0.1
- **Compatibility**: Shopware 6.3.0.0 - 6.5.5.2
- **Support**: shopware@topdata.de

## Features
* High-quality product data from own maintenance
* Cross & upselling
* Article properties & specifications

## Configuration Options

### Basic Settings
1. **Sales Channel**
   - Select the sales channel for which the settings should apply

### Product Data Import
2. **Import Product Name**
   - Specify whether to use our defined product name or your own

3. **Import Product Description**
   - Specify whether to use our defined product description or your own

4. **Product Manufacturer**
   - Specify whether to use our defined product manufacturer or your own

5. **Import EANs**
   - Specify whether to import the article's EAN number

6. **Import OEMs**
   - Specify whether to import the article's OEM number

### Product Images
7. **Product Images**
   - Specify whether to use our product images or your own

8. **Delete Old Images**
   - Specify whether outdated product images should be deleted

### Product Properties
9. **Product Properties**
   - Specify whether to import our defined specifications
   - Examples: capacity, color, product type, etc.

10. **Reference PCDs as Properties**
    - Specify whether reference PCDs should be saved as properties

11. **Reference OEMs as Properties**
    - Specify whether reference OEMs should be saved as properties

### Categories
12. **Product Categories**
    - Specify whether to use our created product groups or your own

13. **Parent Category**
    - Set the main product group for the product categories

### Cross-Selling Options
14. **Similar Products**
    - Enable/disable import of similar products

15. **Alternative Products**
    - Enable/disable import of alternative products

16. **Related Products**
    - Enable/disable import of accessories

17. **Bundled Products**
    - Enable/disable import of product bundles

18. **Variants**
    - Enable/disable import of product variants

19. **Color Variants**
    - Enable/disable import of color variants

20. **Capacity Variants**
    - Enable/disable import of capacity variants

### Display Settings

#### Product Detail Page
21. **Other Colors - Display**
    - Configure the display of color variants

22. **Other Capacities - Display**
    - Configure the display of capacity variants

23. **Product Variants**
    - Enable/disable display of product variants

24. **Other Colors**
    - Enable/disable display of color variants

25. **Other Capacities**
    - Enable/disable display of capacity variants

26. **Alternative Products**
    - Enable/disable display of alternative products

27. **Products in Bundle**
    - Enable/disable display of bundle products

28. **Bundles with Product**
    - Enable/disable display of bundles containing the product

29. **Accessories**
    - Enable/disable display of accessories

30. **Similar Products**
    - Enable/disable display of similar products

#### Product Menu
31. **Product Variants**
    - Enable/disable variant display in menu

32. **Other Colors**
    - Enable/disable color variant display in menu

33. **Other Capacities**
    - Enable/disable capacity variant display in menu

34. **Alternative Products**
    - Enable/disable alternative product display in menu

35. **Products in Bundle**
    - Enable/disable bundle product display in menu

36. **Bundles with Product**
    - Enable/disable bundle display in menu

37. **Accessories**
    - Enable/disable accessories display in menu

38. **Similar Products**
    - Enable/disable similar product display in menu

#### Product List
39. **Product Variants**
    - Enable/disable variant display in list

40. **Other Colors**
    - Enable/disable color variant display in list

41. **Other Capacities**
    - Enable/disable capacity variant display in list

42. **Alternative Products**
    - Enable/disable alternative product display in list

43. **Products in Bundle**
    - Enable/disable bundle product display in list

44. **Bundles with Product**
    - Enable/disable bundle display in list

45. **Accessories**
    - Enable/disable accessories display in list

46. **Similar Products**
    - Enable/disable similar product display in list

================
File: src/Component/Collection.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Component;

use Shopware\Core\Framework\Struct\Collection as BaseCollection;

class Collection extends BaseCollection
{
    
}

================
File: src/Content/Product/Events/AssociatedProductsCriteriaEvent.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Content\Product\Events;

use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Event\NestedEvent;

class AssociatedProductsCriteriaEvent extends NestedEvent
{
    protected Criteria $criteria;
    protected SalesChannelContext $context;
    
    public function __construct(Criteria $criteria, SalesChannelContext $context)
    {
        $this->criteria = $criteria;
        $this->context = $context;
    }
    
    public function getCriteria(): Criteria
    {
        return $this->criteria;
    }

    public function getContext(): Context
    {
        return $this->context->getContext();
    }

    public function getSalesChannelContext(): SalesChannelContext
    {
        return $this->context;
    }
}

================
File: src/Controller/TopFeedApiController.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Controller;

use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Topdata\TopdataFoundationSW6\Util\UtilJsonResponse;
use Topdata\TopdataTopFeedSW6\Service\LinkedProductService;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV1;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV2;
use Topdata\TopdataTopFeedSW6\Service\SettingsService;
use Topdata\TopdataTopFeedSW6\Storefront\Page\CompatibleDevicesWidget\CompatibleDevicesWidgetPageV1Loader;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkedProductsPopupLoader;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkTypeEnum;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\ProductIdRetriever;

#[Route(defaults: ['_routeScope' => ['storefront']])]
class TopFeedApiController extends StorefrontController
{


    public function __construct(
        private readonly LinkedProductsPopupLoader           $linkedProductsPopupLoader,
        private readonly SettingsService                     $settingsService,
        private readonly CompatibleDevicesWidgetPageV1Loader $CompatibleDevicesWidgetPageLoader,
        private readonly ProductIdRetriever                  $productIdRetriever,
        private readonly TopfeedHelperServiceV1                $topfeedHelperServiceV1,
        private readonly LinkedProductService                $linkedProductService
    )
    {
    }

    /**
     * Retrieves linked products for a given product and link type.
     *
     * This method is used to fetch and render linked products in a popup.
     * It's typically called via AJAX to dynamically load related product information.
     *
     * @param Request $request The incoming request object
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return JsonResponse A JSON response containing the rendered HTML for the linked products
     */
    #[Route(
        path: '/topdata-top-feed/products/{linkType}/{productId}',
        name: 'frontend.topdata_top_feed.products',
        defaults: ['XmlHttpRequest' => true],
        methods: ['GET'])
    ]
    public function productsLinked($linkType, $productId, Request $request, SalesChannelContext $salesChannelContext): JsonResponse
    {
        if (!Uuid::isValid($productId)) {
            return UtilJsonResponse::error('Product id invalid');
        }

        $result = ['success' => true];
        $page = $this->linkedProductsPopupLoader->buildLinkedProductsPopupPage(
            $request,
            $salesChannelContext,
            $productId,
            $linkType
        );

        $body = $this->renderStorefront('@TopdataTopFeedSW6/storefront/page/linked-products-popup/index.html.twig', [
            'page' => $page
        ])->getContent();
        $result['body'] = $body;
        $result['title'] = $page->pageTitle;

        $response = new JsonResponse($result);
        $response->headers->set("X-Robots-Tag", "noindex, nofollow");

        return $response;
    }


    /**
     * TODO: linkType and $productId should be passed as parameters
     *
     * Retrieves all linked products for a given product.
     *
     * This method is used to fetch and render all linked products without pagination.
     * It's typically used when displaying a full list of variants or related products.
     *
     * @param Request $request The incoming request object
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return JsonResponse A JSON response containing the rendered HTML for all linked products
     */
    #[Route(
        path: '/topdata-top-feed/list-linked-products-all',
        name: 'frontend.topdata_top_feed.list_linked_products_all',
        defaults: ['XmlHttpRequest' => true],
        methods: ['GET']
    )]
    public function listLinkedProductsAll(Request $request, SalesChannelContext $salesChannelContext): JsonResponse
    {
        $productId = $request->get('productId');
        $linkType = $request->get('linkType');
        $linkTypeEnum = LinkTypeEnum::from($linkType);
        $data = '';
        $products = [];
        $products['variants'] = $this->linkedProductService->getLinkedProducts(
            $linkTypeEnum,
            $productId,
            $salesChannelContext,
            false
        );

        foreach ($products as $key => $value) {
            if (!$value['entities']) {
                unset($products[$key]);
                continue;
            }

            if (
                $linkTypeEnum === LinkTypeEnum::COLOR_VARIANTS
                || $linkTypeEnum === LinkTypeEnum::CAPACITY_VARIANTS
            ) {
                $linkedProductIds = $value['entities']->getIds();
                if (count($linkedProductIds) === 0) {
                    continue;
                }

                $products[$key]['otherDevices'] = $this->topfeedHelperServiceV1->getProductIdsWithOtherDevices(
                    $this->container->get(Connection::class),
                    $productId,
                    $linkedProductIds
                );
            }
        }

        if ($products !== []) {
            $data = $this->renderStorefront('@TopdataTopFeedSW6/storefront/page/list-linked-products/all-variants.html.twig', [
                'products' => $products
            ])->getContent();
        }
        $response = new JsonResponse(['success' => true, 'html' => $data]);
        $response->headers->set("X-Robots-Tag", "noindex, nofollow");
        return $response;
    }


    /**
     * Retrieves compatible devices for a given product.
     *
     * This method is used to fetch and render a list of devices compatible with the specified product.
     * It's typically called via AJAX to dynamically load compatibility information.
     *
     * @param string $productid The ID of the product to find compatible devices for
     * @param Request $request The incoming request object
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return JsonResponse A JSON response containing the rendered HTML for compatible devices
     */
    #[Route(
        path: '/topdata-top-feed/compatible-devices/{productid}',
        name: 'frontend.topdata_top_feed.compatible_devices',
        defaults: ['productid' => '', 'XmlHttpRequest' => true],
        methods: ['GET', 'POST']
    )]
    public function compatibleDevices(string $productid, Request $request, SalesChannelContext $salesChannelContext): JsonResponse
    {
        if (!$productid || !Uuid::isValid($productid)) {
            return UtilJsonResponse::error('Something went wrong');
        }

        $result = ['success' => true];
        $page = $this->CompatibleDevicesWidgetPageLoader->load($request, $salesChannelContext, $productid);
        $result['html'] = $this->renderStorefront('@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-content.html.twig', [
            'page' => $page
        ])->getContent();

        $response = new JsonResponse($result);
        $response->headers->set("X-Robots-Tag", "noindex, nofollow");
        return $response;
    }


    /**
     * TODO: move parts of this to LinkedProductService
     *
     * Retrieves an optimized list of linked products for multiple products.
     *
     * This method is used to fetch and render linked products for multiple products in an optimized manner.
     * It's designed to handle bulk requests efficiently, reducing database queries and improving performance.
     *
     * @param Request $request The incoming request object
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return JsonResponse A JSON response containing the rendered HTML for linked products of multiple products
     */
    #[Route(
        path: '/topdata-top-feed/list-linked-products',
        name: 'frontend.topdata_top_feed.list_linked_products',
        defaults: ['XmlHttpRequest' => true],
        methods: ['GET', 'POST']
    )]
    public function listLinkedProductsOptimized(Request $request, SalesChannelContext $salesChannelContext): JsonResponse
    {
        $limit = $this->settingsService->getInt('listVariantsLimit');
        $productIds = $request->get('ids');
        $data = [];
        $linkedProducts = [];
        $allLinkedProdIds = [];

        if ($this->settingsService->getBool('listHideForVariated') && is_array($productIds)) {
            $realIds = [];
            foreach ($productIds as $id) {
                if (Uuid::isValid($id)) {
                    $realIds[] = $id;
                }
            }
            $realIds = array_unique($realIds);
            $criteria = new Criteria($realIds);
            $criteria->addFilter(
                new NotFilter(NotFilter::CONNECTION_OR, [new EqualsFilter('parentId', null)])
            );
            $productVariations = $this
                ->container
                ->get('sales_channel.product.repository')
                ->search(
                    $criteria,
                    $salesChannelContext
                )
                ->getEntities();
        } else {
            $productVariations = new ProductCollection();
        }

        foreach ($productIds as $id) {
            $products = [];

            foreach ($this->settingsService->getLinkedSettings() as $key => $enabled) {
                $linkTypeEnum = LinkTypeEnum::from($key);
                if (($linkTypeEnum === LinkTypeEnum::COLOR_VARIANTS
                        ||
                        $linkTypeEnum === LinkTypeEnum::CAPACITY_VARIANTS
                    )
                    && $productVariations->get($id)
                ) {
                    continue;
                }
                if ($enabled) {
                    $ids = $this->productIdRetriever->getLinkedProductIdsByLinkType($linkTypeEnum, $id);
                    if (count($ids) === 0) {
                        continue;
                    }
                    $hasMore = $limit && (count($ids) > $limit);
                    if ($limit !== 0) {
                        $ids = array_slice($ids, 0, $limit);
                    }
                    $products[$key] = [
                        'ids'          => $ids,
                        'entities'     => [],
                        'otherDevices' => [],
                        'hasMore'      => $hasMore
                    ];
                    $allLinkedProdIds = array_merge($allLinkedProdIds, $ids);
                }
            }

            foreach ($products as $key => $value) {
                if (
                    $key === LinkTypeEnum::COLOR_VARIANTS->value
                    || $key === LinkTypeEnum::CAPACITY_VARIANTS->value
                ) {
                    $products[$key]['otherDevices'] = $this->topfeedHelperServiceV1->getProductIdsWithOtherDevices(
                        $id,
                        $value['ids']
                    );
                }
            }

            if ($products !== []) {
                $linkedProducts[$id] = $products;
            }
        }

        $allLinkedProdIds = array_unique($allLinkedProdIds);

        if ($allLinkedProdIds !== []) {
            $allLinkedProducts = $this
                ->container
                ->get('sales_channel.product.repository')
                ->search(
                    (new Criteria($allLinkedProdIds))->addAssociations(['prices', 'properties.group', 'manufacturer']),
                    $salesChannelContext
                )
                ->getEntities();
        } else {
            $allLinkedProducts = new ProductCollection();
        }


        foreach ($linkedProducts as $id => $linkedInfo) {
            foreach ($linkedInfo as $linkType => $info) {
                $prods = [];
                foreach ($info['ids'] as $lid) {
                    if ($allLinkedProducts->get($lid)) {
                        $prods[] = $allLinkedProducts->get($lid);
                    }
                }
                if ($prods !== []) {
                    $linkedProducts[$id][$linkType]['entities'] = new ProductCollection($prods);
                }
            }

        }

        foreach ($linkedProducts as $id => $linkedInfo) {
            $data[] = [
                'id'   => $id,
                'html' => $this->renderStorefront('@TopdataTopFeedSW6/storefront/page/list-linked-products/index.html.twig', [
                    'products'        => $linkedInfo,
                    'parentProductId' => $id
                ])->getContent()
            ];
        }

        return new JsonResponse(['success' => true, 'products' => $data]);
    }


}

================
File: src/Resources/app/administration/src/module/sw-category/component/sw-category-view/index.js
================
console.log('[TopFeed] Overriding sw-category-view.html.twig');

import template from './sw-category-view.html.twig';

Shopware.Component.override('sw-category-view', {
    template
});

================
File: src/Resources/app/administration/src/module/sw-category/component/sw-category-view/sw-category-view.html.twig
================
{% block sw_category_view_tabs_seo %}

    {% parent %}

    <sw-tabs-item
            v-show="cmsPage || isPage"
            class="sw-category-detail__tab-cms"
            xxxxroute="{ name: 'sw.category.detail.topfeed' }"
            :route="{ name: 'sw.category.detail.cms' }"
            :title="$t('TopdataTopfeedSW6.tabCategoryTopfeedSettings')"
    >
        {{ $t('TopdataTopfeedSW6.tabCategoryTopfeedSettings') }}
    </sw-tabs-item>


{% endblock %}

================
File: src/Resources/app/administration/src/snippet/de-DE.json
================
{
    "TopdataTopfeedSW6": {
        "notification": {
            "saveSuccessTitle":        "Gespeichert",
            "saveSuccessDescription":  "Einstellungen erfolgreich gespeichert"
        },

        "tabCategoryTopfeedSettings":   "TopFeed",
        "tabCategorySettingsMetaTitle": "TopFeed Einstellungen",
        "settingsCardTitle":            "TopFeed Einstellungen",
        "pluginSettings":               "Nutze Elternkategorie-/Plugin-Einstellungen",
        "import":                       {
            "name":                   "Produktnamen importieren",
            "description":            "Produktbeschreibung importieren",
            "brand":                  "Produkthersteller importieren",
            "EANs":                   "EANs importieren",
            "MPNs":                   "OEMs importieren",
            "pictures":               "Produktbilder importieren",
            "unlinkOldPictures":      "Nicht mehr aktuelle Produktbilder löschen",
            "properties":             "Spezifikationen importieren",
            "PCDsProp":               "Referenz-PCDs als Eigenschaften anzeigen",
            "MPNsProp":               "Referenz-OEMs als Eigenschaften anzeigen",
            "importSimilar":          "Ähnliche Produkte importieren",
            "importAlternates":       "Alternative Produkte importieren",
            "importAccessories":      "Verwandte Produkte (Zubehör) importieren",
            "importBoundles":         "Produktbundles importieren",
            "importVariants":         "Varianten importieren",
            "importColorVariants":    "Farbvarianten importieren",
            "importCapacityVariants": "Kapazitätsvarianten importieren",
            "crossSimilar":           "Ähnliche Produkte als Cross-Selling",
            "crossAlternates":        "Alternative Produkte als Cross-Selling",
            "crossAccessories":       "Verwandte Produkte (Zubehör) als Cross-Selling",
            "crossBoundles":          "Produktbundles als Cross-Selling",
            "crossVariants":          "Varianten als Cross-Selling",
            "crossColorVariants":     "Farbvarianten als Cross-Selling",
            "crossCapacityVariants":  "Kapazitätsvarianten als Cross-Selling"
        }
    }
}

================
File: src/Resources/app/administration/src/snippet/en-GB.json
================
{
    "TopdataTopfeedSW6": {
        "notification": {
            "saveSuccessTitle":        "Saved",
            "saveSuccessDescription":  "Settings saved successfully"
        },
        "tabCategoryTopfeedSettings":   "TopFeed",
        "tabCategorySettingsMetaTitle": "TopFeed settings",
        "settingsCardTitle": "TopFeed Settings",
        "pluginSettings":               "Use parent category (or plugin) settings",
        "import":                       {
            "name":                   "Import product name",
            "description":            "Import product description",
            "brand":                  "Import product brand",
            "EANs":                   "Import EANs",
            "MPNs":                   "Import MPNs",
            "pictures":               "Import product pictures",
            "unlinkOldPictures":      "Delete old images",
            "properties":             "Import product properties",
            "PCDsProp":               "Save reference PCDs as characteristics",
            "MPNsProp":               "Save reference MPNs as characteristics",
            "importSimilar":          "Import similar products",
            "importAlternates":       "Import alternate products",
            "importAccessories":      "Import related products (accessories)",
            "importBoundles":         "Import bundled products",
            "importVariants":         "Variant products cross-selling",
            "importColorVariants":    "Import variants with other colors",
            "importCapacityVariants": "Import variants with other capacities",
            "crossSimilar":           "Similar products cross-selling",
            "crossAlternates":        "Alternate products cross-selling",
            "crossAccessories":       "Accessories cross selling",
            "crossBoundles":          "Bundled products cross-selling",
            "crossVariants":          "Variants cross selling",
            "crossColorVariants":     "Color variant products cross-selling",
            "crossCapacityVariants":  "Capacity variant products cross-selling"
        }
    }
}

================
File: src/Resources/app/administration/src/view/category-detail-topfeed/category-detail-topfeed.html.twig
================
{% block sw_category_detail_topfeed %}
    <div class="sw-category-detail-topfeed">
        {% block sw_category_detail_topfeed_config %}

            <mt-card
                :title="$t('TopdataTopfeedSW6.settingsCardTitle')"
                :isLoading="isLoading"
            >

                <mt-switch
                        v-if="feedSettings"
                        :label="$t('TopdataTopfeedSW6.pluginSettings')"
                        v-model="feedSettings.pluginSettings"
                ></mt-switch>

                <div v-if="feedSettings && !feedSettings.pluginSettings">

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.name')"
                            v-model="feedSettings.importSettings.name"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.description')"
                            v-model="feedSettings.importSettings.description"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.brand')"
                            v-model="feedSettings.importSettings.brand"
                    ></mt-switch>


                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.EANs')"
                            v-model="feedSettings.importSettings.EANs"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.MPNs')"
                            v-model="feedSettings.importSettings.MPNs"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.pictures')"
                            v-model="feedSettings.importSettings.pictures"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.pictures"
                            :label="$t('TopdataTopfeedSW6.import.unlinkOldPictures')"
                            v-model="feedSettings.importSettings.unlinkOldPictures"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.properties')"
                            v-model="feedSettings.importSettings.properties"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.PCDsProp')"
                            v-model="feedSettings.importSettings.PCDsProp"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.MPNsProp')"
                            v-model="feedSettings.importSettings.MPNsProp"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importSimilar')"
                            v-model="feedSettings.importSettings.importSimilar"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importAlternates')"
                            v-model="feedSettings.importSettings.importAlternates"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importAccessories')"
                            v-model="feedSettings.importSettings.importAccessories"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importBoundles')"
                            v-model="feedSettings.importSettings.importBoundles"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importVariants')"
                            v-model="feedSettings.importSettings.importVariants"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importColorVariants')"
                            v-model="feedSettings.importSettings.importColorVariants"
                    ></mt-switch>

                    <mt-switch
                            :label="$t('TopdataTopfeedSW6.import.importCapacityVariants')"
                            v-model="feedSettings.importSettings.importCapacityVariants"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importSimilar"
                            :label="$t('TopdataTopfeedSW6.import.crossSimilar')"
                            v-model="feedSettings.importSettings.crossSimilar"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importAlternates"
                            :label="$t('TopdataTopfeedSW6.import.crossAlternates')"
                            v-model="feedSettings.importSettings.crossAlternates"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importAccessories"
                            :label="$t('TopdataTopfeedSW6.import.crossAccessories')"
                            v-model="feedSettings.importSettings.crossAccessories"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importBoundles"
                            :label="$t('TopdataTopfeedSW6.import.crossBoundles')"
                            v-model="feedSettings.importSettings.crossBoundles"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importVariants"
                            :label="$t('TopdataTopfeedSW6.import.crossVariants')"
                            v-model="feedSettings.importSettings.crossVariants"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importColorVariants"
                            :label="$t('TopdataTopfeedSW6.import.crossColorVariants')"
                            v-model="feedSettings.importSettings.crossColorVariants"
                    ></mt-switch>

                    <mt-switch
                            v-if="feedSettings.importSettings.importCapacityVariants"
                            :label="$t('TopdataTopfeedSW6.import.crossCapacityVariants')"
                            v-model="feedSettings.importSettings.crossCapacityVariants"
                    ></mt-switch>
                </div>
            </mt-card>
        {% endblock %}
    </div>
{% endblock %}

================
File: src/Resources/app/administration/src/view/category-detail-topfeed/category-detail-topfeed.scss
================
.some {
    
}

================
File: src/Resources/app/administration/src/view/category-detail-topfeed/index.js
================
import template from './category-detail-topfeed.html.twig';
import './category-detail-topfeed.scss';
//const { mapState, mapGetters } = Shopware.Component.getComponentHelper();
const {Component, Mixin, Context, Data: {Criteria, EntityCollection}} = Shopware;

Component.register('category-detail-topfeed', {
    template: template,

    inject: ['repositoryFactory'],
    mixins: [
        Mixin.getByName('notification')
//        Mixin.getByName('listing')
    ],

    data() {
        return {
            test:              'test-value',
            isLoading:         true,
            currentLanguageId: Shopware.Context.api.languageId,
            feedSettings:      null, // v-model, fetched from server
            category:          null,
            categoryExtension: null
        };
    },

    metaInfo() {
        return {
            title: this.$t('TopdataTopfeedSW6.tabCategorySettingsMetaTitle')
        };
    },

    props: {
        isLoading: {
            type:     Boolean,
            required: true
        }
    },

    computed: {
        topdataCategoryExtensionRepository() {
            return this.repositoryFactory.create('topdata_category_extension');
        }
    },

    created() {
        this.category = Shopware.Store.get('swCategoryDetail').category;
        this.isLoading = true;
        this.loadCategoryFeedSettings();
        this.isLoading = false;
    },

//    watch: {
//        category() {
//        }
//    },

    methods: {
        loadCategoryFeedSettings() {
            var criteria = new Criteria();
            criteria.addFilter(Criteria.equals('categoryId', this.category.id));
            this.topdataCategoryExtensionRepository.search(criteria, Shopware.Context.api).then((result) => {
                // console.log(result);
                let model = result.first();
                if (model) {
                    this.feedSettings = model;
                } else {
                    this.feedSettings = this.topdataCategoryExtensionRepository.create(Shopware.Context.api);
                    this.feedSettings.pluginSettings = true;
                    this.feedSettings.categoryId = this.category.id;
                    this.feedSettings.importSettings = {
                        name:              false,
                        description:       false,
                        brand:             true,
                        EANs:              true,
                        MPNs:              true,
                        pictures:          true,
                        unlinkOldPictures: false,
                        properties:        true,
                        PCDsProp:          false,
                        MPNsProp:          false,

                        importSimilar:          true,
                        importAlternates:       true,
                        importAccessories:      true,
                        importBoundles:         true,
                        importVariants:         true,
                        importColorVariants:    true,
                        importCapacityVariants: true,

                        crossSimilar:          false,
                        crossAlternates:       false,
                        crossAccessories:      false,
                        crossBoundles:         false,
                        crossVariants:         false,
                        crossColorVariants:    false,
                        crossCapacityVariants: false
                    };
                }
            });
        },

        saveCategoryFeedSettings() {
            console.log("saveCategoryFeedSettings");
            this.isLoading = true;
            this.topdataCategoryExtensionRepository
                .save(this.feedSettings, Shopware.Context.api)
                .then(() => {
                    // this.loadCategoryFeedSettings();
                    this.createNotificationSuccess({
                        title:   this.$t('TopdataTopfeedSW6.notification.saveSuccessTitle'),
                        message: this.$t('TopdataTopfeedSW6.notification.saveSuccessDescription')
                    });
                })
                .finally(() => {
                    this.isLoading = false;
                });
        }
    },
    watch:   {
        feedSettings: {
            handler(newValue, oldValue) {
                if(oldValue === null) {
                    return;
                }
                // console.log("feedSettings changed");
                this.saveCategoryFeedSettings();
            },
            deep:      true,
            immediate: false,
        },
    },
});

================
File: src/Resources/app/administration/src/main.js
================
console.log('%c[TopFeed] main.js loaded', 'color: green; font-weight: bold;');

import './module/sw-category/component/sw-category-view/index';

================
File: src/Resources/app/storefront/src/page/topdata-top-feed.linked-products-list.plugin.js
================
import Plugin from 'src/plugin-system/plugin.class';
import DomAccess from 'src/helper/dom-access.helper';
import HttpClient from 'src/service/http-client.service';

/**
 * NOTE 11/2024 THIS IS NOT USED .. WILL PROBABLY GET REMOVED FROM THE PLUGIN LATER
 *
 * TopdataTopFeedLinkedProductsList Plugin
 *
 * This plugin handles the linked products list for the Topdata Top Feed.
 * It initializes the HTTP client, collects product IDs from the DOM,
 * sends them to the server, and updates the DOM with the server response.
 */
export default class TopdataTopFeedLinkedProductsList extends Plugin {
    static options = {
        /**
         * Selector of Item
         *
         * @type {string}
         */
        itemSelector: 'form[name="TopFeedListLinkedProducts"]',

        /**
         * Selector of ListItem
         *
         * @type {string}
         */
        itemListSelector: '[data-listLinkedProducts]',
    };

    /**
     * Initializes the plugin.
     * Sets up the HTTP client, collects product IDs, and sends them to the server.
     */
    init() {
        this._client = new HttpClient();
        let form = DomAccess.querySelector(document, this.options.itemSelector);

        let path = form.action;
        let data = { ids: [] };
        let listLinkedProducts = DomAccess.querySelectorAll(document, this.options.itemListSelector);

        listLinkedProducts.forEach((linkedProduct) => {
            let id = linkedProduct.dataset.productid;
            if (id) {
                data.ids.push(id);
            }
        });

        if (data.ids.length < 1) {
            return;
        }

        this._client.post(path, JSON.stringify(data), (responseText, request) => {
            if (request.status >= 400) {
                console.log(responseText);
            }
            try {
                const response = JSON.parse(responseText);
                if (response.success === true) {
                    response.products.forEach((k) => {
                        DomAccess.querySelector(document, '[data-listLinkedProducts][data-productid="' + k['id'] + '"]').innerHTML = k['html'];
                    });

                    let clickElements = document.querySelectorAll('.topdata-top-feed-show-more-variants');
                    if (clickElements.length > 0) {
                        clickElements.forEach((clickElement) => {
                            clickElement.addEventListener('click', this.loadShowMoreData.bind(this));
                        });
                    }
                } else {
                    console.log(response);
                }
            } catch (error) {
                console.log(error);
            }
        });
    }

    /**
     * Event handler for loading more data.
     *
     * @param {Event} event - The event object.
     */
    loadShowMoreData(event) {
        event.preventDefault();
        let button = event.currentTarget;
        let path = button.dataset.path;

        let container = DomAccess.querySelector(event.currentTarget.parentNode, '.topdata-top-feed-variants-tiles');

        this._client.get(path, (responseText, request) => {
            if (request.status >= 400) {
                console.log(responseText);
            }
            try {
                const response = JSON.parse(responseText);
                if (response.success === true) {
                    container.innerHTML = response.html;
                } else {
                    console.log(response);
                }
            } catch (error) {
                console.log(error);
            }
        });
    }
}

================
File: src/Resources/app/storefront/src/page/topdata-top-feed.linked-products.plugin.js
================
/**
 * @module TopdataTopFeedLinkedProducts
 * @description This module defines the TopdataTopFeedLinkedProducts plugin, which handles the loading and displaying of linked products in a modal.
 */

import Plugin from 'src/plugin-system/plugin.class';
import DomAccess from 'src/helper/dom-access.helper';
import HttpClient from 'src/service/http-client.service';
import PseudoModalUtil from 'src/utility/modal-extension/pseudo-modal.util';

/**
 * @class TopdataTopFeedLinkedProducts
 * @extends Plugin
 * @description A plugin to handle linked products functionality.
 */
export default class TopdataTopFeedLinkedProducts extends Plugin {
    /**
     * @static
     * @property {Object} options - Default options for the plugin.
     * @property {string} options.text - Specifies the text that is prompted to the user.
     * @property {string} options.itemSelector - Selector of the item.
     */
    static options = {
        text: 'Seems like there\'s nothing more to see here.',
        itemSelector: '.topdata-top-feed-linked-products',
    };

    /**
     * @method init
     * @description Initializes the plugin, sets up the HttpClient, and adds a click event listener to the specified element.
     */
    init() {
        this._client = new HttpClient();
        let clickElement = DomAccess.querySelector(document, this.options.itemSelector);
        clickElement.addEventListener('click', this.loadData.bind(this));
    }

    /**
     * @method loadData
     * @description Handles the click event, sends an HTTP GET request to fetch data, and displays it in a modal.
     * @param {Event} event - The click event.
     */
    loadData(event) {
        event.preventDefault();
        let path = DomAccess.querySelector(document, this.options.itemSelector).dataset.path;

        this._client.get(path, (responseText, request) => {
            if (request.status >= 400) {
                console.log(responseText);
            }
            try {
                const response = JSON.parse(responseText);
                if (response.success === true) {
                    container.innerHTML = response.html;
                    const content = `
                    <div class="js-pseudo-modal-template">
                        <div class="js-pseudo-modal-template-title-element">` + response.title + `</div>
                        <div class="js-pseudo-modal-template-content-element">` + response.body + `</div>
                    </div>
                    `;
                    this.openModal(content)

                } else {
                    console.log(response);
                }
            } catch (error) {
                console.log(error);
            }
        });
    }

    /**
     * @method openModal
     * @description Creates and opens a modal with the provided content.
     * @param {string} content - The HTML content to be displayed in the modal.
     */
    openModal(content) {
        this.modal = new PseudoModalUtil(content);
        this.modal.open();
    }
}

/*
$(document).on('click',SELECTOR+'linked-products', function() {
    let button = $(this);
//    let productid = button.data('productid');
//    let linktype = button.data('linktype');
    let path = button.data('path');

    let container = $(LINKED_PRODUCTS_MODAL).first();
    let containerTitle = container.find('.modal-title');
    let containerBody = container.find('.modal-body');
    showLinkedProductsContentLoading(true);
    if (typeof linkedProductsPopupHttpClient === 'undefined') {
        linkedProductsPopupHttpClient = new HttpClient(window.accessKey, window.contextToken);
    }
    linkedProductsPopupHttpClient.abort();
    linkedProductsPopupHttpClient.get(path, (response) => {
        response = JSON.parse(response);
//        console.log(response);
        if(response.success === true) {
            containerTitle.html(response.title);
            containerBody.html(response.body);
        }
        else {
            containerTitle.html('error');
            containerBody.html('');
//            console.log(response);
        }
        showLinkedProductsContentLoading(false);
    });
    if($(this).closest(SELECTOR +'modal-content').length === 0) {
        $(LINKED_PRODUCTS_MODAL_TRIGGER).click();
    }
    return false;
});
*/

================
File: src/Resources/app/storefront/src/page/topdata-top-feed.product-detail-configurator-select.plugin.js
================
import Plugin from 'src/plugin-system/plugin.class';
import DomAccess from 'src/helper/dom-access.helper';
import HttpClient from 'src/service/http-client.service';

/**
 * TopdataTopFeedProductDetailTab is a custom plugin that handles the product detail configurator select functionality.
 * It listens for changes on a specified item selector and redirects the user to a URL based on the selected option.
 */
export default class TopdataTopFeedProductDetailTab extends Plugin {
    /**
     * Default options for the plugin.
     * @type {Object}
     * @property {string} itemSelector - The CSS selector for the item to attach the event listener to.
     */
    static options = {
        /**
         * Selector of Item
         *
         * @type string
         */
        itemSelector: '.topdata-top-feed-product-select',
    };

    /**
     * Initializes the plugin by setting up the HTTP client and adding the event listener.
     */
    init() {
        this._client = new HttpClient();
        let TopFeedProductDetailTabElement = DomAccess.querySelector(document, this.options.itemSelector);
        TopFeedProductDetailTabElement.addEventListener('change', this.openLink.bind(this));
    }

    /**
     * Event handler for the 'change' event on the item selector.
     * Prevents the default action and redirects the user to the URL specified in the selected option.
     *
     * @param {Event} event - The event object.
     */
    openLink(event) {
        event.preventDefault();

        //var path = DomAccess.querySelector(document, this.options.itemSelector).dataset.path;
        let container = DomAccess.querySelector(document, this.options.itemSelector);
        let url = container.options[container.selectedIndex].value;
        //console.log(container.selectedIndex);
        //console.log(container.options[container.selectedIndex].value);

        if(url === '') {
            return;
        }
        window.location.href = url;
    }
}

================
File: src/Resources/app/storefront/src/page/topdata-top-feed.product-detail-tab.plugin.js
================
import Plugin from 'src/plugin-system/plugin.class';
import DomAccess from 'src/helper/dom-access.helper';
import HttpClient from 'src/service/http-client.service';

/**
 * TopdataTopFeedProductDetailTab is a custom plugin that handles the loading of device data
 * when a specific tab is clicked on the product detail page.
 * 
 * @extends Plugin
 */
export default class TopdataTopFeedProductDetailTab extends Plugin {
    /**
     * Default options for the plugin.
     * 
     * @type {Object}
     * @property {string} text - Specifies the text that is prompted to the user.
     * @property {string} itemSelector - Selector of the item to attach the click event.
     */
    static options = {
        text: 'Seems like there\'s nothing more to see here.',
        itemSelector: '.topdata-top-feed-product-devices-load-tab',
    };

    /**
     * Initializes the plugin by setting up the HttpClient and adding the click event listener.
     */
    init() {
        this._client = new HttpClient();
        let TopFeedProductDetailTabElement = DomAccess.querySelector(document, this.options.itemSelector);
        TopFeedProductDetailTabElement.addEventListener('click', this.loadDeviceData.bind(this));
    }

    /**
     * Event handler for loading device data.
     * 
     * @param {Event} event - The click event.
     */
    loadDeviceData(event) {
        event.preventDefault();
        let path = DomAccess.querySelector(document, this.options.itemSelector).dataset.path;
        let container = DomAccess.querySelector(document, '.topdata-top-feed-product-devices-tab-container');

        this._client.get(path, (responseText, request) => {
            if (request.status >= 400) {
                console.log(responseText);
            }
            try {
                const response = JSON.parse(responseText);
                if (response.success === true) {
                    container.innerHTML = response.html;
                } else {
                    console.log(response);
                }
            } catch (error) {
                console.log(error);
            }
        });
    }
}

================
File: src/Resources/app/storefront/src/scss/base.scss
================
.topdata-top-feed-loading {
    opacity: 0.5;
    pointer-events: none;
}

//.topdata-top-feed-hidden {
//    display: none !important;
//}

a.topdata-top-feed-button {
    text-decoration: none;
    color: #4a545b;
    margin-bottom: 0.5em;
}

a.topdata-top-feed-button:hover {
    text-decoration: none;
    color: #4a545b;
}

a.topdata-top-feed-button-current,
a.topdata-top-feed-button-current:hover,
.topdata-top-feed-button-current {
    background-color: #008490 !important;
    color: #fff !important;
    border: 1px solid #008490 !important;
}

.topdata-top-feed-variants-slider-item-name {
    text-align: center;
}

.topdata-top-feed-variants-slider-item-image {
    position: relative;
    padding-bottom: 100%;
    height: 0;
    width: 100%;
}

.topdata-top-feed-variants-slider-item-image img {
    width: 100%;
    height: auto;
    display: block;
}

.topdata-top-feed-variants-slider-item-image .product-image-placeholder {
    width: 100%;
    height: auto;
}


/* variant tiles */
.topdata-top-feed-variants-tiles {
    display: flex;
    justify-content: flex-start;
    flex-wrap: wrap;
}

.topdata-top-feed-variants-tile {
    position: relative;
    z-index: 1;
    width: 110px;
    height: 110px;
    margin: 0 $spacer $spacer 0;
}

.topdata-top-feed-variants-tile:hover {
    z-index: 10;
}

.topdata-top-feed-variants-tile-inner {
    position: absolute;
    width: 110px;
    max-height: 110px;
    padding: 5px;
    border: 1px solid $border-color;
    border-radius: $border-radius;
    overflow: hidden;
    transition: 0.3s;
    background: #fff;
    left: 0;
    font-size: .75rem;
    text-align: center;
    margin-top: 0;
}

.topdata-top-feed-variants-tile-image {
    width: 98px;
    height: 98px;
    margin: 0 auto;
}

.topdata-top-feed-variants-tile-image > img,
.topdata-top-feed-variants-tile-image > .product-image-placeholder {
    max-width: 100%;
    max-height: 100%;
}

.topdata-top-feed-variants-tile-inner:hover {
    max-height: 500px;
    width: 170px;
    left: -30px;
    margin-top: -10px;
    padding: 15px 5px 10px;
}

.topdata-top-feed-variants-tile-inner .product-price-unit,
.topdata-top-feed-variants-tile-inner .product-price {
    margin: 0;
    height: auto;
}

.topdata-top-feed-variants-tile-inner .btn {
    font-size: 0.75rem;
}

.topdata-top-feed-variants-tile-inner .topdata-product-menu-container {
    display: none !important;
}

.topfinder-devices-compact.topfeed .h3{
    margin-top: 20px;
    margin-bottom: 0px;
    font-size: medium;
}

.topdata-top-feed-variants-other-devices {
    position: absolute;
    width: 22px;
    height: 22px;
    right: 5px;
    top: 5px;
    border-radius: 50%;
    border: 2px solid #777;
    
    .icon > svg {
        top: 0;
        color: #777;
    }
}

.topdata-top-feed-button {
    .topdata-top-feed-variants-other-devices {
        position: relative;
        margin-left: 0.5em;
        right: auto;
        top: auto;
    }
}

================
File: src/Resources/app/storefront/src/main.js
================
import TopdataTopFeedLinkedProducts from "./page/topdata-top-feed.linked-products.plugin";
import TopdataTopFeedLinkedProductsList from './page/topdata-top-feed.linked-products-list.plugin';
import TopdataTopFeedProductDetailTab from './page/topdata-top-feed.product-detail-tab.plugin';
import TopdataTopFeedProductDetailConfigurator from './page/topdata-top-feed.product-detail-configurator-select.plugin';

const PluginManager = window.PluginManager;

PluginManager.register(
    'TopdataTopFeedLinkedProducts',
    TopdataTopFeedLinkedProducts,
    '.topdata-top-feed-linked-products'
);

PluginManager.register(
    'TopdataTopFeedLinkedProductsList',
    TopdataTopFeedLinkedProductsList,
    '.js-listing-wrapper'
);

PluginManager.register(
    'TopdataTopFeedProductDetailTab',
    TopdataTopFeedProductDetailTab,
    '.topdata-top-feed-product-devices-load-tab'
);

PluginManager.register(
    'TopdataTopFeedProductDetailConfigurator',
    TopdataTopFeedProductDetailConfigurator,
    '.topdata-top-feed-product-select'
);

================
File: src/Resources/config/config.xml
================
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/System/SystemConfig/Schema/config.xsd">
    <card>
        <title>Import information from web-service:</title>
        <title lang="de-DE">Konfiguration</title>
        <input-field type="bool">
            <name>productName</name>
            <defaultValue>true</defaultValue>
            <label>Import product name</label>
            <label lang="de-DE">Produktnamen importieren</label>
            <helpText>Specify whether the product name defined by us or your own should be used</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob der von uns definierte Produktname, bzw. ihr eigener Produktname, verwendet werden soll</helpText>
        </input-field>
        <input-field type="single-select">
            <name>productDescriptionImportType</name>
            <defaultValue>NO_IMPORT</defaultValue>
            <label>Product description import type</label>
            <label lang="de-DE">Produktbeschreibung Import-Typ</label>
            <helpText>Specify how product descriptions should be handled during import</helpText>
            <helpText lang="de-DE">Legen Sie fest, wie Produktbeschreibungen beim Import behandelt werden sollen</helpText>
            <options>
                <option>
                    <id>NO_IMPORT</id>
                    <name>Don't import product description</name>
                    <name lang="de-DE">Produktbeschreibungen nicht importieren</name>
                </option>
                <option>
                    <id>REPLACE</id>
                    <name>Replace existing description</name>
                    <name lang="de-DE">Bestehende Beschreibung ersetzen</name>
                </option>
                <option>
                    <id>APPEND</id>
                    <name>Append to existing description</name>
                    <name lang="de-DE">An bestehende Beschreibung anhängen</name>
                </option>
                <option>
                    <id>PREPEND</id>
                    <name>Prepend to existing description</name>
                    <name lang="de-DE">Der bestehenden Beschreibung voranstellen</name>
                </option>
                <option>
                    <id>INJECT</id>
                    <name>Inject into existing description (Markers: TOPDATA_DESCRIPTION_BEGIN and TOPDATA_DESCRIPTION_END)</name>
                    <name lang="de-DE">In bestehende Beschreibung einfügen (Marker: TOPDATA_DESCRIPTION_BEGIN und TOPDATA_DESCRIPTION_END)</name>
                </option>
            </options>
        </input-field>
<!--        <input-field type="bool">-->
<!--            <name>productDescription</name>-->
<!--            <defaultValue>true</defaultValue>-->
<!--            <label>Import product description</label>-->
<!--            <label lang="de-DE">Produktbeschreibung importieren</label>-->
<!--            <helpText>Specify whether the product description defined by us or your own should be used</helpText>-->
<!--            <helpText lang="de-DE">Legen Sie fest, ob die von uns definierte Produktbeschreibung, bzw. ihre eigene Produktbeschreibung, verwendet werden soll</helpText>-->
<!--        </input-field>-->
        <!--input-field type="bool">
            <name>productLongDescription</name>
            <defaultValue>true</defaultValue>
            <label>Product Long Description</label>
            <label lang="de-DE">Produktlangbeschreibung</label>
        </input-field-->
        <input-field type="bool">
            <name>productBrand</name>
            <defaultValue>true</defaultValue>
            <label>Import product brand</label>
            <label lang="de-DE">Produkthersteller importieren</label>
            <helpText>Specify whether the product manufacturer defined by us or your own should be used</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob der von uns definierte Produkthersteller, bzw. der von Ihnen definierte Produkthersteller, verwendet werden soll</helpText>
        </input-field>
        <input-field type="bool">
            <name>productEan</name>
            <defaultValue>true</defaultValue>
            <label>Import EANs</label>
            <label lang="de-DE">EANs importieren</label>
            <helpText>Specify whether the EAN-Number of the Article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob die EAN-Nummer des Artikels für das Mapping verwendet werden soll</helpText>
        </input-field>
        <input-field type="bool">
            <name>productOem</name>
            <defaultValue>true</defaultValue>
            <label>Import MPNs</label>
            <label lang="de-DE">OEMs importieren</label>
            <helpText>Specify whether the MPN-Number of the Article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob die OEM-Nummer des Artikels für das Mapping werden soll</helpText>
        </input-field>
        <input-field type="bool">
            <name>productImages</name>
            <defaultValue>true</defaultValue>
            <label>Import product pictures</label>
            <label lang="de-DE">Produktbilder importieren</label>
            <helpText>Specify whether our product pictures or your own should be used</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob unsere Produktbilder bzw. Ihre eigenen oder keine verwendet werden sollen</helpText>
        </input-field>

        <!-- 11/2024 NOT USED IN TOPFEED ? -->
        <input-field type="bool">
            <name>productImagesDelete</name>
            <defaultValue>false</defaultValue>
            <label>Delete old images</label>
            <label lang="de-DE">Nicht mehr aktuelle Produktbilder löschen</label>
            <helpText>Specify whether product pictures not longer used can be deleted</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob nicht mehr aktuelle Produktbilder gelöscht werden sollen</helpText>
        </input-field>

        <input-field type="bool">
            <name>productSpecifications</name>
            <defaultValue>true</defaultValue>
            <label>Import product properties</label>
            <label lang="de-DE">Spezifikationen importieren</label>
            <helpText>Specify whether specifications defined by us should be imported and displayed (z.B. Capacity, Color, product type, and much more)</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob von uns definierte Spezifikationen (z.B. Kapazität, Farbe, Produktart, u.v.m.) importiert und verwendet werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>specReferencePCD</name>
            <defaultValue>true</defaultValue>
            <label>Save reference PCDs as characteristics</label>
            <label lang="de-DE">Referenz-PCDs als Eigenschaften anzeigen</label>
            <helpText>Specify whether reference PCDs should be displayed</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Referenz-PCDs als Eigenschaft des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>specReferenceOEM</name>
            <defaultValue>true</defaultValue>
            <label>Save reference MPNs as characteristics</label>
            <label lang="de-DE">Referenz-OEMs als Eigenschaften anzeigen</label>
            <helpText>Specify whether reference MPNs should be displayed</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Referenz-OEMs  als Eigenschaft des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>productWaregroups</name>
            <defaultValue>true</defaultValue>
            <label>Import product categories</label>
            <label lang="de-DE">Produktwarengruppen importieren</label>
            <helpText>Specify whether you want to use categories created by us or your own</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Sie von uns angelegte Warengruppen oder Ihre eigenen verwenden möchten</helpText>
        </input-field>
        <input-field type="bool">
            <name>productWaregroupsDelete</name>
            <defaultValue>true</defaultValue>
            <label>Clear categories on import</label>
            <label lang="de-DE">Kategorien beim Import löschen</label>
            <helpText>Unlink all product categories before fetching categories from API</helpText>
            <helpText lang="de-DE">Heben Sie die Verknüpfung aller Produktkategorien auf, bevor Sie Kategorien von der API abrufen </helpText>
        </input-field>
        <component name="sw-entity-single-select">
            <name>productWaregroupsParent</name>
            <entity>category</entity>
            <label>Parent category for all product categories</label>
            <label lang="de-DE">Hauptwarengruppe für die Produktwarengruppen festlegen</label>
            <helpText>Specify the main waregroup for the product waregroups</helpText>
            <helpText lang="de-DE">Legen Sie die Hauptwarengruppe für die Produktkategorien fest</helpText>
        </component>
    </card>
    <card>
        <title>Cross-Selling</title>
        <title lang="de-DE">Cross-Selling</title>
        <input-field type="bool">
            <name>productSimilar</name>
            <defaultValue>true</defaultValue>
            <label>Import similar products</label>
            <label lang="de-DE">Ähnliche Produkte importieren</label>
            <helpText>Specify whether "similar products" to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zu jedem Artikel alternative Produkte importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productSimilarCross</name>
            <defaultValue>false</defaultValue>
            <label>Similar products cross-selling</label>
            <label lang="de-DE">Ähnliche Produkte als Cross-Selling </label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productAlternate</name>
            <defaultValue>true</defaultValue>
            <label>Import alternate products</label>
            <label lang="de-DE">Alternative Produkte importieren</label>
            <helpText>Specify whether "alternate products" to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zu jedem Artikel ähnliche Produkte importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productAlternateCross</name>
            <defaultValue>false</defaultValue>
            <label>Alternate products cross-selling</label>
            <label lang="de-DE">Alternative Produkte als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productRelated</name>
            <defaultValue>true</defaultValue>
            <label>Import related products (accessories)</label>
            <label lang="de-DE">Verwandte Produkte (Zubehör) importieren</label>
            <helpText>Specify whether "accessories" to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zu jedem Artikel verwandte Produkte (Zubehör) importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productRelatedCross</name>
            <defaultValue>false</defaultValue>
            <label>Accessories products cross-selling</label>
            <label lang="de-DE">Verwandte Produkte (Zubehör) als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productBundled</name>
            <defaultValue>true</defaultValue>
            <label>Import bundled products</label>
            <label lang="de-DE">Produktbundles importieren</label>
            <helpText>Specify whether "bundled products" to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zu jedem Artikel Produktbundles importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productBundledCross</name>
            <defaultValue>false</defaultValue>
            <label>Bundled products cross-selling</label>
            <label lang="de-DE">Produktbundles als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariant</name>
            <defaultValue>true</defaultValue>
            <label>Import variants</label>
            <label lang="de-DE">Varianten importieren</label>
            <helpText>Specify whether "variant products" to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Farben eines Artikels importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariantCross</name>
            <defaultValue>false</defaultValue>
            <label>Variant products cross-selling</label>
            <label lang="de-DE">Varianten als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariantColor</name>
            <defaultValue>true</defaultValue>
            <label>Import variants with other colors</label>
            <label lang="de-DE">Farbvarianten importieren</label>
            <helpText>Specify whether "variant products" with different colors to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Farben eines Artikels importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariantColorCross</name>
            <defaultValue>false</defaultValue>
            <label>Color variant products cross-selling</label>
            <label lang="de-DE">Farbvarianten als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariantCapacity</name>
            <defaultValue>true</defaultValue>
            <label>Import variants with other capacities</label>
            <label lang="de-DE">Kapazitätsvarianten importieren</label>
            <helpText>Specify whether "variant products" with different capacities to each article should be imported</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Kapazitäten eines Artikels importiert werden sollen, sofern vorhanden</helpText>
        </input-field>
        <input-field type="bool">
            <name>productVariantCapacityCross</name>
            <defaultValue>false</defaultValue>
            <label>Capacity variant products cross-selling</label>
            <label lang="de-DE">Kapazitätsvarianten als Cross-Selling</label>
            <helpText>Creates cross-selling with manual product selection</helpText>
            <helpText lang="de-DE">Erzeugt Cross-Selling mit manueller Produktauswahl</helpText>
        </input-field>
    </card>
    <card>
        <title>Product Details Page</title>
        <title lang="de-DE">Produktdetailseite</title>
        <input-field type="single-select">
            <name>colorVariants</name>
            <options>
                <option>
                    <id>none</id>
                    <name>Dont show</name>
                    <name lang="de-DE">Nicht zeigen</name>
                </option>
                <option>
                    <id>buttons</id>
                    <name>Buttons</name>
                    <name lang="de-DE">Tasten</name>
                </option>
                <option>
                    <id>slider</id>
                    <name>Slider</name>
                    <name lang="de-DE">Schieberegler</name>
                </option>
                <option>
                    <id>tiles</id>
                    <name>Tiles</name>
                    <name lang="de-DE">Fliesen</name>
                </option>
                <option>
                    <id>menu</id>
                    <name>Drop-down menu</name>
                    <name lang="de-DE">Dropdown-Menü</name>
                </option>
            </options>
            <label>Define displaying of variants with other colors</label>
            <label lang="de-DE">Anzeige der Varianten mit anderen Farben definieren</label>
            <helpText>Specify how variants with other colors should be displayed</helpText>
            <helpText lang="de-DE">Legen Sie fest, wie die Anzeige der Varianten mit anderen Farben erfolgen soll</helpText>
        </input-field>
        <input-field type="single-select">
            <name>capacityVariants</name>
            <options>
                <option>
                    <id>none</id>
                    <name>Dont show</name>
                    <name lang="de-DE">Nicht zeigen</name>
                </option>
                <option>
                    <id>buttons</id>
                    <name>Buttons</name>
                    <name lang="de-DE">Tasten</name>
                </option>
                <option>
                    <id>slider</id>
                    <name>Slider</name>
                    <name lang="de-DE">Schieberegler</name>
                </option>
                <option>
                    <id>tiles</id>
                    <name>Tiles</name>
                    <name lang="de-DE">Fliesen</name>
                </option>
                <option>
                    <id>menu</id>
                    <name>Drop-down menu</name>
                    <name lang="de-DE">Dropdown-Menü</name>
                </option>
            </options>
            <label>Define displaying of variants with other capacities</label>
            <label lang="de-DE">Anzeige der Varianten mit anderen Kapazitäten definieren</label>
            <helpText>Specify how variants with other capacities should be displayed</helpText>
            <helpText lang="de-DE">Legen Sie fest, wie die Anzeige der Varianten mit anderen Kapazitäten erfolgen soll</helpText>
        </input-field>
        <input-field type="bool">
            <name>variantsHideForVariated</name>
            <defaultValue>true</defaultValue>
            <label>Hide API variants and color/capacity tabs for variable product</label>
            <label lang="de-DE">API-Varianten und Registerkarten für Farbe/Kapazität für variable Produkte ausblenden</label>
        </input-field>
    </card>
    <card>
        <title>Product Details Page Tabs</title>
        <title lang="de-DE">Registerkarten der Produktdetailseite</title>
        <input-field type="bool">
            <name>showProductDevicesTab</name>
            <defaultValue>true</defaultValue>
            <label>Show compatible devices</label>
            <label lang="de-DE">Kompatible Geräte anzeigen </label>
            <helpText>Specify whether compatible devices should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob kompatible Geräte auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showVariantProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show variants on product detail page</label>
            <label lang="de-DE">Varianten auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether variants should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showColorVariantProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show variants with other colors on product detail page</label>
            <label lang="de-DE">Varianten mit anderen Farben auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether variants with other colors should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Farben eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showCapacityVariantProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show variants with other capacities on product detail page</label>
            <label lang="de-DE">Varianten mit anderen Kapazitäten auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether variants with other capacities should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Kapazitäten eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showAlternateProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show alternate products on product detail page</label>
            <label lang="de-DE">Alternative Produkte auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether alternate products should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob alternative Produkte eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showBundledProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show artice bundles that contain the product on product detail page</label>
            <label lang="de-DE">Artikel-Bundles, die das Produkt enthalten auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether bundles that contain the product should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zum Bundle zugehörige Produkte auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showBundlesTab</name>
            <defaultValue>true</defaultValue>
            <label>Show products associated with the bundle on product detail page</label>
            <label lang="de-DE">Artikel die im Bundle enthalten sind, auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether products associated with the bundle should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Bundles, die das Produkt enthalten, auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showRelatedProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show accessories on product detail page</label>
            <label lang="de-DE">Verwandte Produkte (Zubehör) auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether accessories should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob verwandte Produkte (Zubehör) eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showSimilarProductsTab</name>
            <defaultValue>true</defaultValue>
            <label>Show similar products on product detail page</label>
            <label lang="de-DE">Ähnliche Produkte auf der Produktdetailseite anzeigen</label>
            <helpText>Specify whether similar products to each article should be displayed on the product detail page</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob ähnliche Produkte eines Artikels auf der Produktdetailseite angezeigt werden sollen</helpText>
        </input-field>
    </card>
    <card>
        <title>Product Menu Items [needs TopdataProductListingMenuSW6 plugin]</title>
        <title lang="de-DE">Produktmenüelemente [benötigt TopdataProductListingMenuSW6-Plugin]</title>
        <input-field type="bool">
            <name>showVariantProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show variants on product menu</label>
            <label lang="de-DE">Varianten im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether variants should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showColorVariantProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show variants with other colors on product menu</label>
            <label lang="de-DE">Varianten mit anderen Farben im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether variants with other colors should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Farben eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showCapacityVariantProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show variants with other capacities on product menu</label>
            <label lang="de-DE">Varianten mit anderen Kapazitäten im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether variants with other capacities should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Kapazitäten eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showAlternateProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show alternate products on product menu</label>
            <label lang="de-DE">Alternative Produkte im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether alternate products should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob alternative Produkte eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showBundledProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show artice bundles that contain the product on product menu</label>
            <label lang="de-DE">Artikel-Bundles, die das Produkt enthalten im Produkt-Menü anzeigen</label>
            <helpText>Specify whether bundles that contain the product should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zum Bundle zugehörige Produkte im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showBundles</name>
            <defaultValue>true</defaultValue>
            <label>Show products associated with the bundle on product menu</label>
            <label lang="de-DE">Artikel die im Bundle enthalten sind im Produkt-Menü anzeigen</label>
            <helpText>Specify whether products associated with the bundle should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Bundles, die das Produkt enthalten, im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showRelatedProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show accessories on product menu</label>
            <label lang="de-DE">Verwandte Produkte (Zubehör) im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether accessories should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob verwandte Produkte (Zubehör) eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>showSimilarProducts</name>
            <defaultValue>true</defaultValue>
            <label>Show similar products on product menu</label>
            <label lang="de-DE">Ähnliche Produkte (Zubehör) im Produkt-Menü des Artikels anzeigen</label>
            <helpText>Specify whether similar products to each article should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob ähnliche Produkte eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>menuHideForVariated</name>
            <defaultValue>true</defaultValue>
            <label>Hide API color capacity menu items for variable product</label>
            <label lang="de-DE">API-Farbkapazitätsmenüelemente für variable Produkte ausblenden</label>
        </input-field>
    </card>
    <card>
        <title>Product List Items</title>
        <title lang="de-DE">Produktlistenelemente</title>
        <input-field type="single-select">
            <name>listVariantsLimit</name>
            <options>
                <option>
                    <id>0</id>
                    <name>none</name>
                </option>
                <option>
                    <id>1</id>
                    <name>1</name>
                </option>
                <option>
                    <id>2</id>
                    <name>2</name>
                </option>
                <option>
                    <id>3</id>
                    <name>3</name>
                </option>
                <option>
                    <id>4</id>
                    <name>4</name>
                </option>
                <option>
                    <id>5</id>
                    <name>5</name>
                </option>
                <option>
                    <id>6</id>
                    <name>6</name>
                </option>
                <option>
                    <id>7</id>
                    <name>7</name>
                </option>
                <option>
                    <id>8</id>
                    <name>8</name>
                </option>
                <option>
                    <id>9</id>
                    <name>9</name>
                </option>
                <option>
                    <id>10</id>
                    <name>10</name>
                </option>
            </options>
            <label>Limit variants per product</label>
            <label lang="de-DE">Maximale Varianten pro Produkt anzeigen</label>
        </input-field>
        <input-field type="bool">
            <name>listVariantProducts</name>
            <defaultValue>false</defaultValue>
            <label>Show variants on product list</label>
            <label lang="de-DE">Varianten in der Produktliste anzeigen</label>
            <helpText>Specify whether variants should be displayed on the product  list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten eines Artikels in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listColorVariantProducts</name>
            <defaultValue>false</defaultValue>
            <label>Show variants with other colors on product list</label>
            <label lang="de-DE">Varianten mit anderen Farben in der Produktliste anzeigen</label>
            <helpText>Specify whether variants with other colors should be displayed on the product list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Farben eines Artikels in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listCapacityVariantProducts</name>
            <defaultValue>false</defaultValue>
            <label>Show variants with other capacities on product list</label>
            <label lang="de-DE">Varianten mit anderen Kapazitäten in der Produktliste anzeigen</label>
            <helpText>Specify whether variants with other capacities should be displayed on the product list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Varianten mit anderen Kapazitäten eines Artikels in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listAlternateProducts</name>
            <defaultValue>false</defaultValue>
            <label>Alternative products</label>
            <label lang="de-DE">Alternative Produkte</label>
            <helpText>Specify whether similar products to each article should be displayed on the product menu</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob ähnliche Produkte eines Artikels im Produkt-Menü des Artikels angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listBundledProducts</name>
            <defaultValue>false</defaultValue>
            <label>Show artice bundles that contain the product on product list</label>
            <label lang="de-DE">Artikel-Bundles, die das Produkt enthalte in der Produktliste anzeigen</label>
            <helpText>Specify whether bundles that contain the product should be displayed on the product list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob zum Bundle zugehörige Produkte in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listBundles</name>
            <defaultValue>false</defaultValue>
            <label>Show products associated with the bundle on product list</label>
            <label lang="de-DE">Artikel die im Bundle enthalten sind in der Produktliste anzeigen</label>
            <helpText>Specify whether products associated with the bundle should be displayed on the product list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob Bundles, die das Produkt enthalten, in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listRelatedProducts</name>
            <defaultValue>false</defaultValue>
            <label>Show accessories on product list</label>
            <label lang="de-DE">Ähnliche Produkte (Zubehör) in der Produktliste anzeigen</label>
            <helpText>Specify whether accessories should be displayed on the product list</helpText>
            <helpText lang="de-DE">Legen Sie fest, ob verwandte Produkte (Zubehör) eines Artikels in der Produktliste angezeigt werden sollen</helpText>
        </input-field>
        <input-field type="bool">
            <name>listHideForVariated</name>
            <defaultValue>true</defaultValue>
            <label>Hide API color capacity variants for variable product</label>
            <label lang="de-DE">API-Farbkapazitätsvarianten für variable Produkte ausblenden</label>
        </input-field>
    </card>
</config>

================
File: src/Resources/config/routes.xml
================
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

<!--    <import resource="../../Controller/TopFeedApiController.php" type="attribute"/>-->
    <import resource="../../Controller/**/*Controller.php" type="attribute"/>

</routes>

================
File: src/Resources/config/services.xml
================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <!-- CONTROLLERS -->
        <service id="Topdata\TopdataTopFeedSW6\Controller\TopFeedApiController" public="true" autowire="true">
            <call method="setContainer">
                <argument type="service" id="service_container"/>
            </call>
<!--            <call method="setTwig">-->
<!--                <argument type="service" id="twig"/>-->
<!--            </call>-->
        </service>

        <!-- SERVICES -->
        <service id="Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkedProductsPopupLoader" public="true" autowire="true">
            <argument key="$productRepository" type="service" id="sales_channel.product.repository"/>
        </service>
        <service id="Topdata\TopdataTopFeedSW6\Service\PageExtensionService" autowire="true"/>
        <service id="Topdata\TopdataTopFeedSW6\Service\ProductPageExtender" autowire="true">
            <argument key="$productRepository" type="service" id="sales_channel.product.repository"/>
        </service>
        <service id="Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV2" autowire="true"/>
        <service id="Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV1" autowire="true"/>
        <service id="Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\ProductIdRetriever" autowire="true"/>
        <service id="Topdata\TopdataTopFeedSW6\Service\SettingsService" autowire="true"/>
        <service id="Topdata\TopdataTopFeedSW6\Service\LinkedProductService" autowire="true">
            <argument type="service" key="$productRepository" id="sales_channel.product.repository"/>
        </service>

        <service id="Topdata\TopdataTopFeedSW6\Storefront\Page\CompatibleDevicesWidget\CompatibleDevicesWidgetPageV1Loader" autowire="true">
            <argument type="service" key="$productRepository" id="sales_channel.product.repository"/>
        </service>



        <!-- EVENT SUBSCRIBERS -->
        <service id="Topdata\TopdataTopFeedSW6\Subscriber\ProductPageCriteriaSubscriber" autowire="true">
            <tag name="kernel.event_subscriber"/>
        </service>

        <service id="Topdata\TopdataTopFeedSW6\Subscriber\ProductPageLoadedEventSubscriber" autowire="true">
            <tag name="kernel.event_subscriber"/>
        </service>

        <service id="Topdata\TopdataTopFeedSW6\Subscriber\ProductMenuLoadedEventSubscriber" autowire="true">
            <tag name="kernel.event_subscriber"/>
        </service>



        <!-- SNIPPETS -->
        <service id="Topdata\TopdataTopFeedSW6\Resources\snippet\en_GB\SnippetFile_en_GB">
            <tag name="shopware.snippet.file" priority="100"/>
        </service>
        <service id="Topdata\TopdataTopFeedSW6\Resources\snippet\de_DE\SnippetFile_de_DE">
            <tag name="shopware.snippet.file" priority="100"/>
        </service>

    </services>
</container>

================
File: src/Resources/snippet/de_DE/SnippetFile_de_DE.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Resources\snippet\de_DE;

use Shopware\Core\System\Snippet\Files\SnippetFileInterface;

class SnippetFile_de_DE implements SnippetFileInterface
{
    public function getName(): string
    {
        return 'storefront.de-DE';
    }

    public function getPath(): string
    {
        return __DIR__ . '/storefront.de-DE.json';
    }

    public function getIso(): string
    {
        return 'de-DE';
    }

    public function getAuthor(): string
    {
        return 'TopData Software GmbH';
    }

    public function isBase(): bool
    {
        return false;
    }
}

================
File: src/Resources/snippet/de_DE/storefront.de-DE.json
================
{
    "topdata-topfeed": {
        "colors": "Farben",
        "capacities": "Kapazitäten",
        "deviceManufacturer": "Gerätehersteller",
        "deviceSeries": "Geräteserie",
        "deviceTypes": "Gerätetypen",
        "devicesNoType": "Geräte ohne Typ",
        "differentDevices": "Kompatible Geräte können abweichen",
        "list": {
            "alternate": "Alternativ Artikel",
            "bundled": "Artikel in Bundles enthalten",
            "related": "Zubehör",
            "similar": "Ähnliche Artikel",
            "bundles": "Bundles mit Artikeln",
            "color": "Andere Farben",
            "capacity": "Andere Kapazitäten",
            "variants": "Produktvarianten",
            "showMoreVariants":"zeige alle"
        },
        "product": {
            "notFound": "Artikel nicht gefunden!",
            "alternate": "Alternativ Artikel",
            "bundled": "Artikel in Bundles enthalten",
            "related": "Zubehör",
            "similar": "Ähnliche Artikel",
            "bundles": "Bundles mit Artikeln",
            "colors": "Andere Farben",
            "capacity": "Andere Kapazitäten",
            "variants": "Produktvarianten",
            "devices": "Kompatible Geräte"
        },
        "for": "für",
        "menu": {
            "alternate": "Alternativ",
            "bundled": "Bundleinhalt",
            "related": "Zubehör",
            "similar": "Ähnlich",
            "bundles": "in Bundles",
            "colors": "Andere Farben",
            "capacity": "Andere Kapazitäten",
            "variants": "Produktvarianten"
        }
    }
}

================
File: src/Resources/snippet/en_GB/SnippetFile_en_GB.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Resources\snippet\en_GB;

use Shopware\Core\System\Snippet\Files\SnippetFileInterface;

class SnippetFile_en_GB implements SnippetFileInterface
{
    public function getName(): string
    {
        return 'storefront.en-GB';
    }

    public function getPath(): string
    {
        return __DIR__ . '/storefront.en-GB.json';
    }

    public function getIso(): string
    {
        return 'en-GB';
    }

    public function getAuthor(): string
    {
        return 'TopData Software GmbH';
    }

    public function isBase(): bool
    {
        return false;
    }
}

================
File: src/Resources/snippet/en_GB/storefront.en-GB.json
================
{
    "topdata-topfeed": {
        "colors": "Colors",
        "capacities": "Capacities",
        "deviceManufacturer": "Device Manufacturer",
        "deviceSeries": "Device Series",
        "deviceTypes": "Device Types",
        "devicesNoType": "Devices Without Type",
        "differentDevices": "Compatible devices may differ",
        "list": {
            "alternate": "Alternate Products",
            "bundled": "Products in Bundle",
            "related": "Accessories",
            "similar": "Similar Products",
            "bundles": "Bundles with Product",
            "color": "Other Colors",
            "capacity": "Other Capacities",
            "variants": "Product Variants",
            "showMoreVariants":"show all"
        },
        "product": {
            "notFound": "Product not found!",
            "alternate": "Alternate Products",
            "bundled": "Products in Bundle",
            "related": "Accessories",
            "similar": "Similar Products",
            "bundles": "Bundles with Product",
            "colors": "Other Colors",
            "capacity": "Other Capacities",
            "variants": "Product Variants",
            "devices": "Compatible Devices"
        },
        "for": "for",
        "menu": {
            "alternate": "Alternate",
            "bundled": "Bundle contents",
            "related": "Accessories",
            "similar": "Similar",
            "bundles": "In bundles",
            "colors": "Other colors",
            "capacity": "Other capacities",
            "variants": "Product variants"
        }
    }
}

================
File: src/Resources/views/storefront/component/product/card/box-standard.html.twig
================
{% sw_extends '@Storefront/storefront/component/product/card/box-standard.html.twig' %}
{% block component_product_box_info %}
    {{ parent() }}
    {% block topfeed_list_linked_products %}
    {% endblock %}
{% endblock %}

================
File: src/Resources/views/storefront/element/cms-element-product-description-reviews.html.twig
================
{% sw_extends '@Storefront/storefront/element/cms-element-product-description-reviews.html.twig' %}

{% set variant_products = page.extensions.associated_products.elements.variant_products.elements %}
{% set color_variant_products = page.extensions.associated_products.elements.color_variant_products.elements %}
{% set capacity_variant_products = page.extensions.associated_products.elements.capacity_variant_products.elements %}
{% set alternate_products = page.extensions.associated_products.elements.alternate_products.elements %}
{% set bundled_products = page.extensions.associated_products.elements.bundled_products.elements %}
{% set bundles = page.extensions.associated_products.elements.bundles.elements %}
{% set related_products = page.extensions.associated_products.elements.related_products.elements %}
{% set similar_products = page.extensions.associated_products.elements.similar_products.elements %}
{# {% set feed_product_devices = page.extensions.feed_product_devices %} #}
{% set listingColumns = 'col-sm-6 col-lg-4 col-xl-3' %}

{# for some reason the layout variable was set to "boxed", but only "standard", "image", "minimal", "wishlist" are supported by default by sw66. #}
{# Use the original layout if it's one of the supported types, otherwise fall back to minimal #}
{% set supportedLayouts = ['standard', 'image', 'minimal', 'wishlist'] %}
{% set myProductBoxLayout = layout in supportedLayouts ? layout : 'minimal' %}




{# TAB HEADERS - APPEND AFTER THE "REVIEW" TAB OUR TABS #}
{% block element_product_description_reviews_tabs_navigation_review %}

    {{ parent() }}


    {# TAB HEADER: TOPDATA DEBUG #}
{#    <li class="nav-item">#}
{#        <a class="nav-link product-detail-tab-navigation-link"#}
{#           id="topdata-debug-tab"#}
{#           data-bs-toggle="tab"#}
{#           data-off-canvas-tabs="true"#}
{#           href="#topdata-debug-tab-pane"#}
{#           role="tab"#}
{#           aria-selected="false"#}
{#        >#}
{#            TOPDATA DEBUG#}
{#        </a>#}
{#    </li>#}

    {# TAB HEADER: DEVICES #}
    {% if page.extensions.TopdataTopFeedSW6.productHasDevices %}
        {% block product_topfeed_tab_navigation_devices %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link topdata-top-feed-product-devices-load-tab"
                   id="feed-devices-tab"
                   data-productid="{{ page.product.id }}"
                   data-path="{{ path('frontend.topdata_top_feed.compatible_devices', {productid: page.product.id}) }}"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#feed-devices-tab-pane"
                   role="tab"
                   aria-controls="feed-devices-tab-pane"
                   aria-selected="true"
                >
                    {% block product_topfeed_tab_navigation_devices_inner %}
                        {{ "topdata-topfeed.product.devices"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}

    {# TAB HEADER: VARIANTS #}
    {% if(variant_products) %}
        {% block product_topfeed_tab_navigation_variants %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="variant-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#variant-tab-pane"
                   role="tab"
                   aria-controls="variant-tab-pane"
                   aria-selected="true"
                >
                    {% block product_topfeed_tab_navigation_variants_inner %}
                        xxx
                        {{ "topdata-topfeed.product.variants"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                        {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                    </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(color_variant_products) %}
        {% block product_topfeed_tab_navigation_colors %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="color-variant-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#color-variant-tab-pane"
                   role="tab"
                   aria-controls="color-variant-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_colors_inner %}
                        {{ "topdata-topfeed.product.colors"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(capacity_variant_products) %}
        {% block product_topfeed_tab_navigation_capacities %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="capacity-variant-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#capacity-variant-tab-pane"
                   role="tab"
                   aria-controls="capacity-variant-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_capacities_inner %}
                        {{ "topdata-topfeed.product.capacity"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(alternate_products) %}
        {% block product_topfeed_tab_navigation_alternates %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="alternate-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#alternate-tab-pane"
                   role="tab"
                   aria-controls="alternate-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_alternates_inner %}
                        {{ "topdata-topfeed.product.alternate"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(bundled_products) %}
        {% block product_topfeed_tab_navigation_in_bundle %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="bundled-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#bundled-tab-pane"
                   role="tab"
                   aria-controls="bundled-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_in_bundle_inner %}
                        {{ "topdata-topfeed.product.bundled"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(bundles) %}
        {% block product_topfeed_tab_navigation_bundles %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="bundles-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#bundles-tab-pane"
                   role="tab"
                   aria-controls="bundles-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_bundles_inner %}
                        {{ "topdata-topfeed.product.bundles"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(related_products) %}
        {% block product_topfeed_tab_navigation_accessories %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="related-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#related-tab-pane"
                   role="tab"
                   aria-controls="related-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_accessories_inner %}
                        {{ "topdata-topfeed.product.related"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
    {% if(similar_products) %}
        {% block product_topfeed_tab_navigation_similars %}
            <li class="nav-item">
                <a class="nav-link product-detail-tab-navigation-link"
                   id="similar-tab"
                   data-bs-toggle="tab"
                   data-off-canvas-tabs="true"
                   href="#similar-tab-pane"
                   role="tab"
                   aria-controls="similar-tab-pane"
                   aria-selected="true">
                    {% block product_topfeed_tab_navigation_similars_inner %}
                        {{ "topdata-topfeed.product.similar"|trans|sw_sanitize }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    {% endblock %}
                </a>
            </li>
        {% endblock %}
    {% endif %}
{% endblock %}




{# ==== TAB CONTENTS ==== #}
{% block element_product_description_reviews_tabs_content_review %}
    {{ parent() }}


{#    #}{# TAB CONTENT: TOPDATA DEBUG #}
{#    <div class="tab-pane fade show "#}
{#         id="topdata-debug-tab-pane"#}
{#         role="tabpanel"#}
{#    >#}

{#        <h2>page.extensions.TopdataTopFeedSW6</h2>#}
{#        {{ dump(page.extensions.TopdataTopFeedSW6) }}#}

{#        <h2>page.extensions.associated_products</h2>#}
{#        {{ dump(page.extensions.associated_products) }}#}

{#        <div>#}
{#            layout: {{ layout }}<br>#}
{#            myProductBoxLayout: {{ myProductBoxLayout }}<br>#}
{#        </div>#}

{#        {{ dump(topConfigNested('TopdataTopFeedSW6')) }}#}
{#        #### {{ dump(topConfigGetBool('TopdataTopFeedSW6', 'menu.variantColors')) }} #####}

{#        <h2>topConfigNested('TopdataTopFeedSW6')</h2>#}
{#        {{ dump(topConfigNested('TopdataTopFeedSW6')) }}#}

{#        <h2>topConfigFlat('TopdataTopFeedSW6')</h2>#}
{#        {{ dump(topConfigFlat('TopdataTopFeedSW6')) }}#}

{#        <h2>topConfigToml('TopdataTopFeedSW6')</h2>#}
{#        <pre>{{ topConfigToml('TopdataTopFeedSW6') }}</pre>#}

{#        <h2>ORIGINAL TopdataTopFeedSW6.config</h2>#}
{#        {{ dump(config('TopdataTopFeedSW6.config')) }}#}

{#    </div>#}

    {# TAB CONTENT: DEVICES #}
    {% if page.extensions.TopdataTopFeedSW6.productHasDevices %}
        <div class="tab-pane fade show"
             id="feed-devices-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_devices %}
                {% sw_include '@Storefront/storefront/page/product-detail/feed-devices-tab.html.twig' %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: VARIANT PRODUCTS #}
    {% if(variant_products) %}
        <div class="tab-pane fade show"
             id="variant-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_variants %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: variant_products
                } %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: COLOR VARIANTS #}
    {% if(color_variant_products) %}
        <div class="tab-pane fade show"
             id="color-variant-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_color_variants %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: color_variant_products
                } %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: CAPACITY VARIANTS #}
    {% if(capacity_variant_products) %}
        <div class="tab-pane fade show"
             id="capacity-variant-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_capacity_variants %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: capacity_variant_products,
                } %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: ALTERNATE PRODUCTS #}
    {% if(alternate_products) %}
        <div class="tab-pane fade show"
             id="alternate-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_alternates %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: alternate_products,
                } %}
            {% endblock %}
        </div>
    {% endif %}


    {# TAB CONTENT: BUNDLED PRODUCTS #}
    {% if(bundled_products) %}
        <div class="tab-pane fade show"
             id="bundled-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_in_bundle %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: bundled_products,
                } %}
            {% endblock %}
        </div>
    {% endif %}


    {# TAB CONTENT: BUNDLES #}
    {% if(bundles) %}
        <div class="tab-pane fade show"
             id="bundles-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_bundles %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: bundles,
                } %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: RELATED PRODUCTS #}
    {% if(related_products) %}
        <div class="tab-pane fade show"
             id="related-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_accessories %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: related_products,
                } %}
            {% endblock %}
        </div>
    {% endif %}

    {# TAB CONTENT: SIMILAR PRODUCTS #}
    {% if(similar_products) %}
        <div class="tab-pane fade show"
             id="similar-tab-pane"
             role="tabpanel"
        >
            {% block product_topfeed_tab_similars %}
                {% sw_include '@Storefront/storefront/page/product-detail/products-tab.html.twig' with {
                    layout: myProductBoxLayout,
                    products: similar_products
                } %}
            {% endblock %}
        </div>
    {% endif %}

{% endblock %}

================
File: src/Resources/views/storefront/layout/header/header.html.twig
================
{#
This template extends the default header template from the Storefront.
It adds additional functionality to the header actions block.
#}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}

{% block layout_header_actions %}
    {{ parent() }} {# Include the parent block content #}
{% endblock %}

================
File: src/Resources/views/storefront/page/linked-products-popup/index.html.twig
================
{% set listingColumns = 'col-sm-6 col-lg-4' %}
<div class="cms-element-product-listing-wrapper">
    <div class="cms-element-product-listing">
        <div class="row cms-listing-row js-listing-wrapper">
            {% block linked_products_popup_products %}
                {% for product in page.products %}
                    <div class="cms-listing-col {{ listingColumns }}">
                        {% sw_include '@Storefront/storefront/component/product/card/box.html.twig' %}
                    </div>
                {% endfor %}
            {% endblock %}
        </div>
        {% block linked_products_popup_extension %}{% endblock %}
    </div>
</div>

================
File: src/Resources/views/storefront/page/list-linked-products/all-variants.html.twig
================
{% for key,element in products %}
    {% for product in element.entities %}
        {% set otherDevices = false %}
        {% if product.id in element.otherDevices %}
            {% set otherDevices = true %}
        {% endif %}
        {% sw_include '@TopdataTopFeedSW6/storefront/page/list-linked-products/product.html.twig' with {
            otherDevices: otherDevices
        } %}
    {% endfor %}
{% endfor %}

================
File: src/Resources/views/storefront/page/list-linked-products/index.html.twig
================
{% block topfeed_list_linked_products_index %}
    {% for key,element in products %}
        <div class="listlinkedproducts-{{ key }}">
            {% set title = 'topdata-topfeed.list.'~key %}
            <h6>{{ title|trans }}</h6>
            <div class="topdata-top-feed-variants-tiles">
            {% for product in element.entities %}
                {% set otherDevices = false %}
                {% if product.id in element.otherDevices %}
                    {% set otherDevices = true %}
                {% endif %}
                {% sw_include '@TopdataTopFeedSW6/storefront/page/list-linked-products/product.html.twig' with {
                    otherDevices: otherDevices
                } %}
            {% endfor %}
            </div>
            {% if element.hasMore %}
                <a class="topdata-top-feed-show-more-variants"
                   data-path="{{ path('frontend.topdata_top_feed.list_linked_products_all', {
                            'productId':parentProductId,
                            'linkType':key
                        }) }}"
                   href="#">{{ 'topdata-topfeed.list.showMoreVariants'|trans }}</a>
            {% endif %}
        </div>
    {% endfor %}
{% endblock %}

================
File: src/Resources/views/storefront/page/list-linked-products/product.html.twig
================
{% block topfeed_variants_tile %}
    <div class="topdata-top-feed-variants-tile">
        <div class="topdata-top-feed-variants-tile-inner">
            {% if otherDevices %}
                <div class="topdata-top-feed-variants-other-devices"
                      data-toggle="tooltip"
                      title="{{ 'topdata-topfeed.differentDevices'|trans }}"
                     >
                    {% sw_icon 'exclamationmark' style {'pack':'solid', 'size': 'fluid'} %}
                </div>
            {% endif %}
            <a href="{{ seoUrl('frontend.detail.page', {'productId': product.id}) }}"
               class="topdata-top-feed-variants-tile-url"
               title="{{ product.name }}"
               >
                {% block topfeed_variants_tile_image %}
                    <div class="topdata-top-feed-variants-tile-image">
                        {% if product.cover.media.url %}
                            {% sw_thumbnails 'product-image-thumbnails' with {
                                    media: product.cover.media,
                                    sizes: {
                                        'xs': '284px',
                                        'sm': '284px',
                                        'md': '284px',
                                        'lg': '284px',
                                        'xl': '284px'
                                    }
                                } %}
                        {% else %}
                            <div class="product-image-placeholder">
                                {% sw_icon 'placeholder' style {
                                    'size': 'fluid'
                                } %}
                            </div>
                        {% endif %}
                    </div>
                {% endblock %}
            </a>
            <div class="topdata-top-feed-variants-tile-meta">
                {% block topfeed_variants_tile_name %}
                    <div class="topdata-top-feed-variants-tile-title">
                        {{ product.name }}
                    </div>
                {% endblock %}
                {% sw_include '@Storefront/storefront/component/product/card/price-unit.html.twig' %}
                {% sw_include '@Storefront/storefront/component/product/card/action.html.twig' %}
            </div>
        </div>
    </div>
{% endblock %}

================
File: src/Resources/views/storefront/page/product-detail/buy-widget.html.twig
================
{% sw_extends '@Storefront/storefront/page/product-detail/buy-widget.html.twig' %}

{% set color_variant_products = page.extensions.product_variants.colors %}
{% set color_variant_type = page.extensions.product_variants.colors_type %}
{% set capacity_variant_products = page.extensions.product_variants.capacities %}
{% set capacity_variant_type = page.extensions.product_variants.capacities_type %}

{% block page_product_detail_configurator_include %}
    {{ parent() }}
    {% if color_variant_products|length %}
        {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/variants.html.twig' with {
            type: color_variant_type,
            products: color_variant_products,
            title: 'topdata-topfeed.product.colors'|trans,
            variant_type: 'color',
            otherDevices: page.extensions.productColorsOtherDevices.all
        } %}
    {% endif %}
    {% if capacity_variant_products|length %}
        {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/variants.html.twig' with {
            type: capacity_variant_type,
            products: capacity_variant_products,
            title: 'topdata-topfeed.product.capacity'|trans,
            variant_type: 'capacity',
            otherDevices: page.extensions.productCapacitiesOtherDevices.all
        } %}
    {% endif %}
{% endblock %}

================
File: src/Resources/views/storefront/page/product-detail/feed-devices-content.html.twig
================
{% set devices = page.devices %}
{% set brandName = page.product.manufacturer.name %}
{% set deviceClass = 'col-lg-3 col-md-4 col-sm-6 col-xs-12' %}
{% set sufix = random() %}
{% if devices %}
    <div class="card card-tabs product-detail-tabs">
        {% if page.uniqueSeries or page.uniqueTypes %}
        <div class="card-header product-detail-tab-navigation">
            <ul class="nav nav-tabs product-detail-tab-navigation-list"
                 id="product-compatible-devices-tabs"
                 role="tablist">
                <li class="nav-item">
                    <a class="nav-link product-detail-tab-navigation-link active"
                       id="tab1-tab{{ sufix }}"
                       data-bs-toggle="tab"
                       {# @deprecated tag:v6.6.0 - Registering plugin on selector "data-offcanvas-tabs" is deprecated. Use "data-off-canvas-tabs" instead #}
                        {% if feature('v6.6.0.0') %}
                            data-off-canvas-tabs="true"
                        {% else %}
                            data-offcanvas-tabs="true"
                        {% endif %}
                       href="#tab1-tab-pane{{ sufix }}"
                       role="tab"
                       aria-controls="tab1-tab-pane{{ sufix }}"
                       aria-selected="true">
                        {{ 'topdata-topfeed.deviceManufacturer'|trans }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    </a>
                </li>
                {% if page.uniqueSeries %}
                <li class="nav-item">
                    <a class="nav-link product-detail-tab-navigation-link"
                        id="tab2-tab{{ sufix }}"
                        data-bs-toggle="tab"
                        {# @deprecated tag:v6.6.0 - Registering plugin on selector "data-offcanvas-tabs" is deprecated. Use "data-off-canvas-tabs" instead #}
                        {% if feature('v6.6.0.0') %}
                           data-off-canvas-tabs="true"
                        {% else %}
                            data-offcanvas-tabs="true"
                        {% endif %}
                        href="#tab2-tab-pane{{ sufix }}"
                        role="tab"
                        aria-controls="tab2-tab-pane{{ sufix }}"
                        aria-selected="true">
                        {{ 'topdata-topfeed.deviceSeries'|trans }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    </a>
                </li>
                {% endif %}
                {% if page.uniqueTypes %}
                <li class="nav-item">
                    <a class="nav-link product-detail-tab-navigation-link"
                        id="tab3-tab{{ sufix }}"
                        data-bs-toggle="tab"
                        {# @deprecated tag:v6.6.0 - Registering plugin on selector "data-offcanvas-tabs" is deprecated. Use "data-off-canvas-tabs" instead #}
                        {% if feature('v6.6.0.0') %}
                            data-off-canvas-tabs="true"
                        {% else %}
                            data-offcanvas-tabs="true"
                        {% endif %}
                        href="#tab3-tab-pane{{ sufix }}"
                        role="tab"
                        aria-controls="tab3-tab-pane{{ sufix }}"
                        aria-selected="true">
                        {{ 'topdata-topfeed.deviceTypes'|trans }}
                        <span class="product-detail-tab-navigation-icon">
                            {% sw_icon 'arrow-medium-right' style {'pack':'solid'} %}
                        </span>
                    </a>
                </li>
                {% endif %}
            </ul>
        </div>
        {% endif %}
        <div class="product-detail-tabs-content card-body">
            <div class="tab-content">
                <div class="tab-pane fade show active"
                     id="tab1-tab-pane{{ sufix }}"
                     role="tabpanel"
                     aria-labelledby="tab1-tab-pane{{ sufix }}">
                    <button class="btn btn-light btn-block offcanvas-close js-offcanvas-close sticky-top">    {% sw_icon 'x' style { 'size': 'sm' } %}    {{ "general.offcanvasCloseMenu"|trans|sw_sanitize }}</button>
                    <div class="offcanvas-content-container">
                        <div class="topfinder-devices-compact topfeed row">
                            {% if brandName %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: brandName, 
                                        notBrandName: '', 
                                        groupMode: 'brands'
                                    }
                                %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: '', 
                                        notBrandName: brandName, 
                                        groupMode: 'brands'
                                    }
                                %}
                            {% else %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: '', 
                                        notBrandName: '', 
                                        groupMode: 'brands'
                                    }
                                %}
                            {% endif %}
                        </div>
                    </div>
                </div>
                {% if page.uniqueSeries %}
                <div class="tab-pane fade show"
                     id="tab2-tab-pane{{ sufix }}"
                     role="tabpanel"
                     aria-labelledby="tab2-tab-pane{{ sufix }}">
                    <button class="btn btn-light btn-block offcanvas-close js-offcanvas-close sticky-top">    {% sw_icon 'x' style { 'size': 'sm' } %}    {{ "general.offcanvasCloseMenu"|trans|sw_sanitize }}</button>
                    <div class="offcanvas-content-container">
                        <div class="topfinder-devices-compact topfeed row">
                            {% if brandName %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: brandName, 
                                        notBrandName: '', 
                                        groupMode: 'series'
                                    }
                                %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: '', 
                                        notBrandName: brandName, 
                                        groupMode: 'series'
                                    }
                                %}
                            {% else %}
                                {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list.html.twig' 
                                    with {
                                        brandName: '', 
                                        notBrandName: '', 
                                        groupMode: 'series'
                                    }
                                %}
                            {% endif %}
                        </div>
                    </div>
                </div>
                {% endif %}
                {% if page.uniqueTypes %}
                <div class="tab-pane fade show"
                     id="tab3-tab-pane{{ sufix }}"
                     role="tabpanel"
                     aria-labelledby="tab3-tab-pane{{ sufix }}">
                    <button class="btn btn-light btn-block offcanvas-close js-offcanvas-close sticky-top">    {% sw_icon 'x' style { 'size': 'sm' } %}    {{ "general.offcanvasCloseMenu"|trans|sw_sanitize }}</button>
                    <div class="offcanvas-content-container">
                        <div class="topfinder-devices-compact topfeed row">
                            {% sw_include '@TopdataTopFeedSW6/storefront/page/product-detail/feed-devices-list-type.html.twig' 
                                with {
                                    types: page.uniqueTypes,
                                    noTypeDevicesCount: page.noTypeDevicesCount
                                } 
                            %}
                        </div>
                    </div>
                </div>
                {% endif %}
            </div>
        </div>
    </div>
{% else %}
    <div class="container product-detail-tabs">
        <div class="topfinder-devices-compact topfeed row">
            {{ 'topdata-topfeed.deviceListFoundNothing'|trans }}
        </div>
    </div>
{% endif %}

================
File: src/Resources/views/storefront/page/product-detail/feed-devices-list-type.html.twig
================
{% for type in types %}
    <div class="col-12">
        <div class="topfinder-types-label h3">
            {{ type }}
        </div>
    </div>
    {% for device in devices %}
        {% if device.type_name == type %}
            <div class="col-lg-4 col-sm-6 col-xs-12" >
                {{ device.brand_name ~ ' ' ~device.name }}
            </div>
        {% endif %}
    {% endfor %}
{% endfor %}
{% if noTypeDevicesCount %}
    <div class="col-12">
        <div class="topfinder-types-label h3">
            {{ 'topdata-topfeed.devicesNoType'|trans }}
        </div>
    </div>
    {% for device in devices %}
        {% if not (device.type_id) %}
            <div class="col-lg-4 col-sm-6 col-xs-12" >
                {{ device.brand_name ~ ' ' ~device.name }}
            </div>
        {% endif %}
    {% endfor %}
{% endif %}

================
File: src/Resources/views/storefront/page/product-detail/feed-devices-list.html.twig
================
{% set brandid = '0' %}
{% set seriesid = '0' %}
{% for device in devices %}
    {% if (brandName and (device.brand_name == brandName)) 
          or (notBrandName and (device.brand_name != notBrandName)) 
          or ((brandName == '') and (notBrandName == '')) %}
        {% if groupMode == 'brands' %}
            {% if device.brand_id != brandid %}
                {% set brandid = device.brand_id %}
                <div class="col-12">
                    <div class="h3">{{ device.brand_name }}</div>
                </div>
            {% endif %}
        {% elseif groupMode == 'series' %}
            {% if device.series_id != seriesid %}
                {% set seriesid = device.series_id %}
                <div class="col-12">
                    <div class="h3">{{ device.brand_name ~ ' ' ~ device.series_name }}</div>
                </div>
            {% endif %}
        {% endif %}
        <div class="col-lg-4 col-sm-6 col-xs-12" >
            {{ device.brand_name ~ ' ' ~device.name }}
        </div>
    {% endif %}
{% endfor %}

================
File: src/Resources/views/storefront/page/product-detail/feed-devices-tab.html.twig
================
{% sw_extends '@Storefront/storefront/utilities/offcanvas.html.twig' %}

{% block utilities_offcanvas_content %}
    {% block page_product_detail_feed_devices_container %}
        <div class="topdata-top-feed-product-devices-tab-container"></div>
    {% endblock %}
{% endblock %}

================
File: src/Resources/views/storefront/page/product-detail/products-tab.html.twig
================
{% sw_extends '@Storefront/storefront/utilities/offcanvas.html.twig' %}


{% set listingColumns = 'col-sm-6 col-lg-4 col-xl-3' %}


{% block utilities_offcanvas_content %}
    {% block page_product_detail_top_finder_devices_container %}
        <div class="cms-element-product-listing-wrapper">
            <div class="cms-element-product-listing">
                <div class="row cms-listing-row js-listing-wrapper">

{#                    <h1>~~~~{{ products|length }} Products~~~</h1>#}
{#                    <h1>~~~~{{ dump({otherDevices: otherDevices}) }}~~~</h1>#}


                    {% for product in products %}
                        <div class="cms-listing-col {{ listingColumns }}">
                            {% if product.id in otherDevices %}
                                {{ 'topdata-topfeed.differentDevices'|trans }}
                            {% endif %}
                            {% sw_include '@Storefront/storefront/component/product/card/box.html.twig' %}
                        </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    {% endblock %}
{% endblock %}

================
File: src/Resources/views/storefront/page/product-detail/variants.html.twig
================
<div class="product-detail-configurator-group">
    <div class="product-detail-configurator-group-title">
        {{ title }}
    </div>
    {% if type=='buttons' %}
        <div class="product-detail-configurator-options">
            {% for product in products %}
                <div class="product-detail-configurator-option">
                    <span class="product-detail-configurator-option-input is-combinable"></span>
                    <a href="{{ seoUrl('frontend.detail.page', {'productId': product.id}) }}" 
                       class="product-detail-configurator-option-label is-combinable is-display-text topdata-top-feed-button {% if product.current %}topdata-top-feed-button-current{% endif %}"
                       title="{{ product.name }}"
                       >
                        {{ product.shortName ?:product.name }}
                        {% if product.id in otherDevices %}
                            <div class="topdata-top-feed-variants-other-devices"
                                  data-toggle="tooltip"
                                  title="{{ 'topdata-topfeed.differentDevices'|trans }}"
                                 >
                                {% sw_icon 'exclamationmark' style {'pack':'solid', 'size': 'fluid'} %}
                            </div>
                        {% endif %}
                    </a>
                </div>
            {% endfor %}
        </div>
    {% elseif type=='tiles' %}
        <div class="topdata-top-feed-variants-tiles">
            {% for product in products %}
                {% if not product.current %}
                <div class="topdata-top-feed-variants-tile">
                    <div class="topdata-top-feed-variants-tile-inner">
                        {% if product.id in otherDevices %}
                            <div class="topdata-top-feed-variants-other-devices"
                                  data-toggle="tooltip"
                                  title="{{ 'topdata-topfeed.differentDevices'|trans }}"
                                 >
                                {% sw_icon 'exclamationmark' style {'pack':'solid', 'size': 'fluid'} %}
                            </div>
                        {% endif %}
                        <a href="{{ seoUrl('frontend.detail.page', {'productId': product.id}) }}" 
                           class="topdata-top-feed-variants-tile-url"
                           title="{{ product.name }}"
                           >
                            <div class="topdata-top-feed-variants-tile-image">
                                {% if product.cover.media.url %}
                                    {% sw_thumbnails 'product-image-thumbnails' with {
                                            media: product.cover.media,
                                            sizes: {
                                                'xs': '284px',
                                                'sm': '284px',
                                                'md': '284px',
                                                'lg': '284px',
                                                'xl': '284px'
                                            }
                                        } %}
                                {% else %}
                                    <div class="product-image-placeholder">
                                        {% sw_icon 'placeholder' style {
                                            'size': 'fluid'
                                        } %}
                                    </div>
                                {% endif %}
                            </div>
                        </a>
                        <div class="topdata-top-feed-variants-tile-meta">
                            <div class="topdata-top-feed-variants-tile-title">
                                {{ product.name }}
                            </div>
                            {% sw_include '@Storefront/storefront/component/product/card/price-unit.html.twig' with {product:product.product} %}
                            {% sw_include '@Storefront/storefront/component/product/card/action.html.twig' with {product:product.product} %}
                        </div>
                    </div>
                </div>
                {% endif %}
            {% endfor %}
        </div>
    {% elseif type=='slider' %}
        <div class="topdata-top-feed-variants-slider">
            <div class="cms-element-product-slider">
                <div class="cms-element-alignment align-self-start">
                    <div class="base-slider product-slider has-nav"
                         data-product-slider="true"
                         data-product-slider-options='{
                         "productboxMinWidth": "100px",
                         "slider": {
                         "gutter": 30,
                         "autoplayButtonOutput": false,
                         "nav": false,
                         "controls": true,
                         "autoplay": false
                         }
                         }'>
                        <div class="product-slider-container"
                             data-product-slider-container="true">
                            {% for product in products %}
                                {% if not product.current %}
                                <div class="product-slider-item">
                                    <a href="{{ seoUrl('frontend.detail.page', {'productId': product.id}) }}" 
                                       class="topdata-top-feed-variants-slider-item"
                                       title="{{ product.shortName ?:product.name }}">
                                        <div class="topdata-top-feed-variants-slider-item-image">
                                            {% if product.id in otherDevices %}
                                                <div class="topdata-top-feed-variants-other-devices"
                                                      data-toggle="tooltip"
                                                      title="{{ 'topdata-topfeed.differentDevices'|trans }}"
                                                     >
                                                    {% sw_icon 'exclamationmark' style {'pack':'solid', 'size': 'fluid'} %}
                                                </div>
                                            {% endif %}
                                            {% if product.cover.media.url %}
                                                <img src="{{ product.cover.media.url }}" alt="" />
                                            {% else %}
                                                <div class="product-image-placeholder">
                                                    {% sw_icon 'placeholder' style {
                                                        'size': 'fluid'
                                                    } %}
                                                </div>
                                            {% endif %}
                                        </div>
                                        <div class="topdata-top-feed-variants-slider-item-name">{{ product.name }}</div>
                                    </a>
                                </div>
                                {% endif %}
                            {% endfor %}
                        </div>
                        <div class="product-slider-controls-container">
                            <div class="base-slider-controls"
                                 data-product-slider-controls="true">
                                <button class="base-slider-controls-prev product-slider-controls-prev">
                                    {% sw_icon 'arrow-head-left' %}
                                </button>
                                <button class="base-slider-controls-next product-slider-controls-next">
                                    {% sw_icon 'arrow-head-right' %}
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    {% elseif type=='menu' %}
        <div class="row">
            <div class="col col-lg-6">
                <div class="form-group">
                    <select 
                        class="custom-select topdata-top-feed-product-select" 
                        name="topdataTopFeedProduct" 
                        value="">
                        {% for product in products %}
                            {% if product.current %}
                            <option value="">
                                {{ product.shortName ?:product.name }}
                            </option>
                            {% endif %}
                        {% endfor %}
                        {% for product in products %}
                            {% if not product.current %}
                            <option value="{{ seoUrl('frontend.detail.page', {'productId': product.id}) }}">
                                {{ product.shortName ?:product.name }}
                                {{ product.id in otherDevices ? '(!)':'' }}
                            </option>
                            {% endif %}
                        {% endfor %}
                    </select>
                </div>
            </div>
            {% if otherDevices %}
                <div class="col-12">
                    (!) - {{ 'topdata-topfeed.differentDevices'|trans }}
                </div>
            {% endif %}
        </div>
    {% endif %}
</div>

================
File: src/Service/LinkedProductService.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Service;

use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkTypeEnum;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\ProductIdRetriever;


/**
 * 11/2024 created (extracted from TopFeedApiController)
 */
class LinkedProductService
{


    public function __construct(
        private readonly ProductIdRetriever     $productIdRetriever,
        private readonly SalesChannelRepository $productRepository,
        private readonly SettingsService        $settingsService, // TODO: replace this with TopConfigRegistry
    )
    {
    }

    /**
     * 11/2024 moved from TopFeedApiController to here
     *
     * Retrieves linked products based on the specified link type and product ID.
     *
     * This private method is used internally to fetch different types of linked products
     * such as variants, alternate products, bundled products, etc.
     *
     * @param LinkTypeEnum $linkType The type of link (e.g., variants, alternate, bundled)
     * @param string $productId The ID of the main product
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @param bool $limit Whether to limit the number of returned products
     * @return array An array containing the linked products and a flag indicating if there are more
     */
    public function getLinkedProducts(LinkTypeEnum $linkType, string $productId, SalesChannelContext $salesChannelContext, bool $limit = true): array
    {
        $productIds = $this->productIdRetriever->getProductIdsUsingSettings($linkType, $productId);

        if ($productIds === []) {
            return ['hasMore' => false, 'entities' => null];
        }

        $productsCount = count($productIds);
        if ($limit) {
            $limit = $this->settingsService->getInt('listVariantsLimit');
            if ($limit > 0) {
                $productIds = array_slice($productIds, 0, $limit);
            }
        }
        $hasMore = ($productsCount > count($productIds));

        return [
            'hasMore'  => $hasMore,
            'entities' => $this->productRepository->search(
                (new Criteria($productIds))->addAssociations([
                    'prices',
                    'properties.group'
                ]),
                $salesChannelContext
            )->getEntities()
        ];
    }

}

================
File: src/Service/PageExtensionService.php
================
<?php

namespace Topdata\TopdataTopFeedSW6\Service;

use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\Framework\Struct\ExtendableInterface;

/**
 * for extending pages, but could be used for other things too
 *
 * 10/2024 created
 */
class PageExtensionService
{
    private string $extensionName; // eg 'TopdataTopFeedSW6';
    private ExtendableInterface $page;

    /**
     * 10/2024 created
     */
    public function init(string $extensionName, ExtendableInterface $page): void
    {
        $this->extensionName = $extensionName;
        $this->page = $page;
    }

    /**
     * Get or create an extension on a page.
     *
     * @param ExtendableInterface $this->page
     * @return ArrayStruct
     */
    public function getOrCreateExtension(): ArrayStruct
    {
        $extension = $this->page->getExtension($this->extensionName);

        if ($extension === null) {
            $extension = new ArrayStruct();
            $this->page->addExtension($this->extensionName, $extension);
        }

        return $extension;
    }

    /**
     * Add or update data in an extension.
     *
     * @param ExtendableInterface $this->page
     * @param array $data
     * @return ArrayStruct
     */
    public function updateExtension(array $data): ArrayStruct
    {
        $extension = $this->getOrCreateExtension();
        $extension->assign($data);

        return $extension;
    }

    /**
     * Get a value from an extension, with a default if not set.
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function getExtensionValue(string $key, $default = null)
    {
        $extension = $this->page->getExtension($this->extensionName);

        if ($extension === null || !$extension->has($key)) {
            return $default;
        }

        return $extension->get($key);
    }
}

================
File: src/Service/ProductPageExtender.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Service;

use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Product\ProductPage;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Topdata\TopdataConnectorSW6\Constants\MergedPluginConfigKeyConstants;
use Topdata\TopdataTopFeedSW6\Component\Collection;
use Topdata\TopdataTopFeedSW6\Content\Product\Events\AssociatedProductsCriteriaEvent;

/**
 * 10/2024 created (extracted from ProductPageLoadedEventSubscriber)
 */
class ProductPageExtender
{


    private array $colorVariantIds;
    private array $capacityVariantIds;
    // ----
    private SalesChannelProductEntity $product;
    private ProductPage $page;
    private SalesChannelContext $salesChannelContext;


    public function __construct(
        private readonly SettingsService          $settingsService,
        private readonly TopfeedHelperServiceV1   $topfeedHelperService,
        private readonly SalesChannelRepository   $productRepository,
        private readonly EventDispatcherInterface $eventDispatcher,
        private readonly PageExtensionService     $pageExtensionService,
        private readonly Connection               $connection,
    )
    {
    }


    public function init(ProductPage $page, SalesChannelContext $salesChannelContext): void
    {
        $this->page = $page;
        $this->salesChannelContext = $salesChannelContext;
        $this->product = $page->getProduct();
        if ($this->product->getParentId() && $this->settingsService->getBool('variantsHideForVariated')) {
            $this->colorVariantIds = [];
            $this->capacityVariantIds = [];
        } else {
            $this->colorVariantIds = $this->topfeedHelperService->getColorVariantProductIds($this->product->getId(), $this->product->getParentId());
            $this->capacityVariantIds = $this->topfeedHelperService->getCapacityVariantProductIds($this->product->getId(), $this->product->getParentId());
        }
        // ---- page extension service for easily extend the product page
        $this->pageExtensionService->init('TopdataTopFeedSW6', $page);
    }


    /**
     * Adds associated products to the product page.
     *
     */
    public function addAssociatedProducts(): void
    {
        $currentProductId = $this->product->getId();
        $parentProductId = $this->product->getParentId();
        $struct = new Collection();

        $associatedProductIds = [
            'alternate_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showAlternateProductsTab) ?
                $this->topfeedHelperService->getAlternateProductIds($currentProductId, $parentProductId) : false,

            'bundled_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showBundledProductsTab) ?
                $this->topfeedHelperService->getBundledProductIds($currentProductId, $parentProductId) : false,

            'bundles' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showBundlesTab) ?
                $this->topfeedHelperService->getBundleIds($currentProductId, $parentProductId) : false,

            'capacity_variant_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showCapacityVariantProductsTab) ?
                $this->capacityVariantIds : false,

            'color_variant_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showColorVariantProductsTab) ?
                $this->colorVariantIds : false,

            'variant_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showVariantProductsTab) ?
                $this->topfeedHelperService->getVariantProductIds($currentProductId, $parentProductId) : false,

            'related_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showRelatedProductsTab) ?
                $this->topfeedHelperService->getRelatedProductIds($currentProductId, $parentProductId) : false,

            'similar_products' => $this->settingsService->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showSimilarProductsTab) ?
                $this->topfeedHelperService->getSimilarProductIds($currentProductId, $parentProductId) : false,
        ];

        $productIds = [];

        foreach ($associatedProductIds as $el) {
            if ($el) {
                $productIds = array_merge($productIds, $el);
            }
        }

        $productIds = array_unique($productIds);

        if ($productIds !== []) {
            $associatedProducts = $this->_getProductsCollection($productIds);
        }

        foreach ($associatedProductIds as $key => $el) {
            if ($el) {
                $prods = [];
                foreach ($el as $id) {
                    if ($associatedProducts->get($id)) {
                        $prods[] = $associatedProducts->get($id);
                    }
                }
                if ($prods !== []) {
                    $struct->set($key, new ProductCollection($prods));
                }

            }
        }

        $this->page->addExtension('associated_products', $struct);
    }


    /**
     * Adds product variants (colors and capacities) to the product page.
     *
     */
    public function addProductVariants(): void
    {
        $struct = [];
        $struct['colors_type'] = $this->settingsService->getString('colorVariants');
        $struct['colors'] = [];
        if ($struct['colors_type'] !== 'none') {
            $productIds = $this->colorVariantIds;
            if ($productIds !== []) {
                $products = $this->productRepository->search(
                    (new Criteria($productIds))
                        ->addAssociation('prices')
                        ->addAssociation('cover')
                        ->addAssociation('properties.group'),
                    $this->salesChannelContext
                )->getEntities();
                foreach ($products as $product) {
                    /** @var SalesChannelProductEntity $product */

                    $prodProperties = $product->getProperties();
                    $colors = [];
                    $capacities = [];
                    foreach ($prodProperties as $property) {

                        $propertyGroupName = $property->getGroup()->getName();
                        if (($translation = $property->getGroup()->getTranslated()) && isset($translation['name'])) {
                            $propertyGroupName = $translation['name'];
                        }

                        $propertyName = $property->getName();
                        if (($translation = $property->getTranslated()) && isset($translation['name'])) {
                            $propertyName = $translation['name'];
                        }

                        if (in_array($propertyGroupName, $this->_colorNames())) {


                            $colors[] = $this->_filterColor($propertyName);
                        }
                        if (in_array($propertyGroupName, $this->_capacityNames())) {
                            $capacities[] = $this->_filterColor($propertyName);
                        }
                    }

                    $productName = $this->_getProductName($product ?? null);

                    $struct['colors'][] = [
                        'id'        => $product->getId(),
                        'color'     => implode(', ', $colors),
                        'capacity'  => implode(', ', $capacities),
                        'name'      => $productName,
                        'cover'     => $product->getCover(),
                        'current'   => false,
                        'delta'     => $this->_stringDelta($productName, $this->product->getName()),
                        'product'   => $product,
                        'shortName' => implode(', ', $colors)
                    ];
                }

                $colors = [];
                $capacities = [];
                foreach ($this->product->getProperties() as $property) {

                    $propertyGroupName = $property->getGroup()->getName();
                    if (($translation = $property->getGroup()->getTranslated()) && isset($translation['name'])) {
                        $propertyGroupName = $translation['name'];
                    }

                    $propertyName = $property->getName();
                    if (($translation = $property->getTranslated()) && isset($translation['name'])) {
                        $propertyName = $translation['name'];
                    }


                    if (in_array($propertyGroupName, $this->_colorNames())) {
                        $colors[] = $this->_filterColor($propertyName);
                    }
                    if (in_array($propertyGroupName, $this->_capacityNames())) {
                        $capacities[] = $this->_filterColor($propertyName);
                    }
                }


                $productName = $this->_getProductName($product ?? null);

                $current = [
                    'id'        => $this->product->getId(),
                    'color'     => implode(', ', $colors),
                    'capacity'  => implode(', ', $capacities),
                    'name'      => $productName,
                    'cover'     => $this->product->getCover(),
                    'current'   => true,
                    'delta'     => 0,
                    'product'   => $this->product,
                    'shortName' => implode(', ', $colors)
                ];

                //delete other capacities
                foreach ($struct['colors'] as $key => $item) {
                    if ($item['capacity'] !== $current['capacity']) {
                        unset($struct['colors'][$key]);
                    }
                }

                //unique colors (try to find same product version)
                $uniq = [];
                foreach ($struct['colors'] as $key => $item) {
                    $found = false;
                    foreach ($uniq as $k => $v) {
                        if ($v['color'] === $item['color']) {
                            if ($v['delta'] > $item['delta']) {
                                $uniq[$k] = $item;
                            }
                            $found = true;
                            break;
                        }
                    }
                    if (!$found) {
                        $uniq[] = $item;
                    }
                }
                $struct['colors'] = $uniq;

                if ($struct['colors'] !== []) {
                    $struct['colors'][] = $current;

                    usort($struct['colors'], fn($a, $b): int => $a['color'] <=> $b['color']);
                }
            }
        }

        $struct['capacities_type'] = $this->settingsService->getString('capacityVariants');
        $struct['capacities'] = [];
        if ($struct['capacities_type'] !== 'none') {
            $productIds = $this->capacityVariantIds;
            if ($productIds !== []) {
                $products = $this
                    ->productRepository
                    ->search(
                        (new Criteria($productIds))
                            ->addAssociation('cover')
                            ->addAssociation('properties.group'),
                        $this->salesChannelContext
                    )
                    ->getEntities();
                foreach ($products as $product) {
                    /** @var SalesChannelProductEntity $product */

                    $prodProperties = $product->getProperties();
                    $colors = [];
                    foreach ($prodProperties as $property) {

                        $propertyGroupName = $property->getGroup()->getName();
                        if (($translation = $property->getGroup()->getTranslated()) && isset($translation['name'])) {
                            $propertyGroupName = $translation['name'];
                        }

                        $propertyName = $property->getName();
                        if (($translation = $property->getTranslated()) && isset($translation['name'])) {
                            $propertyName = $translation['name'];
                        }

                        if (in_array($propertyGroupName, $this->_capacityNames())) {
                            $colors[] = $this->_filterColor($propertyName);
                        }
                    }

                    $productName = $this->_getProductName($product ?? null);

                    $struct['capacities'][] = [
                        'id'        => $product->getId(),
                        'capacity'  => implode(', ', $colors),
                        'name'      => $productName,
                        'cover'     => $product->getCover(),
                        'current'   => false,
                        'product'   => $product,
                        'shortName' => implode(', ', $colors)
                    ];
                }

                $colors = [];
                foreach ($this->product->getProperties() as $property) {

                    $propertyGroupName = $property->getGroup()->getName();
                    if (($translation = $property->getGroup()->getTranslated()) && isset($translation['name'])) {
                        $propertyGroupName = $translation['name'];
                    }

                    $propertyName = $property->getName();
                    if (($translation = $property->getTranslated()) && isset($translation['name'])) {
                        $propertyName = $translation['name'];
                    }

                    if (in_array($propertyGroupName, $this->_capacityNames())) {
                        $colors[] = $propertyName;
                    }
                }

                $productName = $this->_getProductName($product ?? null);


                $struct['capacities'][] = [
                    'id'        => $this->product->getId(),
                    'capacity'  => implode(', ', $colors),
                    'name'      => $productName,
                    'cover'     => $this->product->getCover(),
                    'current'   => true,
                    'product'   => $this->product,
                    'shortName' => implode(', ', $colors)
                ];

                usort($struct['capacities'], fn($a, $b): int => $a['capacity'] <=> $b['capacity']);
            }
        }

        $this->page->addExtension('product_variants', new ArrayEntity($struct));
    }


    /**
     * Adds 'feed_product_devices' extension to the product page if the product or its parent has associated devices.
     * DONE: the injected variable should be called TopdataTopFeedSW6_productHasDevices
     *
     */
    public function addProductHasDevices(): void
    {
        // Check if product devices tab should be shown
        if (!$this->settingsService->getBool('showProductDevicesTab')) {
            return;
        }

        // Check for devices associated with the current product
        $deviceExists = $this->_checkDeviceExists($this->product->getId());

        // If no devices found and product has a parent, check the parent product
        if (!$deviceExists && $this->product->getParentId()) {
            $deviceExists = $this->_checkDeviceExists($this->product->getParentId());
        }

        // If devices found, add the extension to the page - it is just used as a boolean flag in twig..
        $this->pageExtensionService->updateExtension([
            'productHasDevices' => $deviceExists
        ]);
    }

    /**
     * Adds information about other devices for product variants to the product page.
     *
     */
    public function addProductVariantOtherDevices(): void
    {
        if ($this->colorVariantIds) {
            $productIds = $this->topfeedHelperService->getProductIdsWithOtherDevices(
                $this->product->getId(),
                $this->colorVariantIds
            );
            $this->page->addExtension('productColorsOtherDevices', new ArrayEntity($productIds));
        }

        if ($this->capacityVariantIds) {
            $productIds = $this->topfeedHelperService->getProductIdsWithOtherDevices(
                $this->product->getId(),
                $this->capacityVariantIds
            );
            $this->page->addExtension('productCapacitiesOtherDevices', new ArrayEntity($productIds));
        }
    }

    /**
     * Returns an array of color-related property names.
     *
     * @return array
     */
    private function _colorNames(): array
    {
        return [
            'Farbe',
            'Color',
        ];
    }

    /**
     * IT DOES NOTHING. REMOVE THIS? OR FIX IT?
     *
     * Filters the color string.
     *
     * @param string $stringWithColor
     * @return string
     */
    private function _filterColor(string $stringWithColor): string
    {
        return $stringWithColor;
    }


    /**
     * Retrieves a collection of products based on given product IDs
     *
     * @param array $productIds
     * @param SalesChannelContext $salesChannelContext
     * @return ProductCollection|array
     */
    private function _getProductsCollection($productIds)
    {
        if (!$productIds) {
            return [];
        }
        $criteria = new Criteria($productIds);
        $this->eventDispatcher->dispatch(
            new AssociatedProductsCriteriaEvent($criteria, $this->salesChannelContext)
        );

//            $criteria->addAssociation('properties.group');
//            $criteria->addSorting(new FieldSorting('properties.group.name', FieldSorting::DESCENDING));
//            $criteria->addAssociation('manufacturer');
        $criteria->addSorting(new FieldSorting('manufacturer.name', FieldSorting::ASCENDING));
        $criteria->addSorting(new FieldSorting('name', FieldSorting::ASCENDING));

        return $this
            ->productRepository
            ->search($criteria, $this->salesChannelContext)
            ->getEntities();

    }

    /**
     * Calculates the difference between two strings.
     *
     * @param string $str1
     * @param string $str2
     * @return int
     */
    private function _stringDelta($str1, $str2): int
    {
        if (!is_string($str1) || !is_string($str2)) {
            return 0;
        }
        $str1 = trim($str1);
        $str2 = trim($str2);
        if ($str1 === $str2) {
            return 0;
        }
        $strlen1 = strlen($str1);
        $strlen2 = strlen($str2);
        if ($strlen1 === 0) {
            return $strlen2 ?: 0;
        }

        if ($strlen2 === 0) {
            return $strlen1 ?: 0;
        }

        $deltaStart = 0;
        $deltaEnd = 0;
        for ($i = 0; $i < $strlen1; $i++) {
            if ($i === $strlen1 - 1 && $strlen2 > $strlen1) {
                return $strlen2 - $strlen1;
            }

            if ($i === $strlen2 - 1 && $strlen1 > $strlen2) {
                return $strlen1 - $strlen2;
            }

            if ($str1[$i] !== $str2[$i]) {
                $deltaStart = $i;
                break;
            }
        }

        while (true) {
            $strlen1--;
            $strlen2--;
            if ($strlen1 < 0 && $strlen2 >= 0) {
                return $strlen2 - $strlen1;
            }
            if ($strlen2 < 0 && $strlen1 >= 0) {
                return $strlen1 - $strlen2;
            }
            if ($strlen2 < 0 && $strlen1 < 0) {
                return 0;
            }
            if ($str1[$strlen1] !== $str2[$strlen2]) {
                $deltaEnd = max($strlen1, $strlen2);
                break;
            }
        }

        return ($deltaStart > $deltaEnd) ? ($deltaStart + 1 - $deltaEnd) : ($deltaEnd + 1 - $deltaStart);
    }

    /**
     * Returns an array of capacity-related property names.
     *
     * @return array
     */
    private function _capacityNames(): array
    {
        return [
            'Kapazität (Zusatz)',
            'Kapazität',
            'Capacity',
        ];
    }

    /**
     * Checks if a device exists for a given product ID.
     *
     * @param string $productId
     * @return bool
     */
    private function _checkDeviceExists(string $productId): bool
    {
        $query = 'SELECT device_id FROM topdata_device_to_product WHERE product_id = 0x' . $productId . ' LIMIT 1';
        $result = $this->connection->executeQuery($query)->fetchAllAssociative();
        return count($result) > 0;
    }

    /**
     * 07/2025 created as a hotfix: if product is null, we use an empty string
     */
    public function _getProductName($product): string
    {
        if ($product) {
            $productName = $product->getName();
            if (($translation = $product->getTranslated()) && isset($translation['name'])) {
                $productName = $translation['name'];
            }
        } else {
            $productName = '';
        }

        return $productName;
    }


}

================
File: src/Service/SettingsService.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Service;

use Shopware\Core\System\SystemConfig\SystemConfigService;
use Topdata\TopdataConnectorSW6\Constants\MergedPluginConfigKeyConstants;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkTypeEnum;

/**
 * LEGACY - will get replaced by the new TopConfigRegistry
 */
class SettingsService
{
    private array $config; // the original config from SW6 native system config

    public function __construct(
        SystemConfigService $systemConfigService
    )
    {
        $this->config = $systemConfigService->get('TopdataTopFeedSW6.config');
    }

    /**
     * uses the old legacy keys
     */
    public function getBool(string $key): bool
    {
        return isset($this->config[$key]) ? (bool)$this->config[$key] : false;
    }

    /**
     * uses the old legacy keys
     */
    public function getString(string $key): string
    {
        return isset($this->config[$key]) ? (string)$this->config[$key] : '';
    }

    /**
     * uses the old legacy keys
     */
    public function getInt(string $key, bool $notNegative = false): int
    {
        $ret = isset($this->config[$key]) ? (int)$this->config[$key] : 0;

        if ($notNegative && ($ret < 0)) {
            return 0;
        }

        return $ret;
    }

    public function getConfig()
    {
        return $this->config;
    }

    /**
     * 10/2024 moved from TopFeedApiController to here
     *
     * Retrieves the settings for linked products.
     *
     * This private method is used internally to get the configuration settings
     * for different types of linked products (variants, alternates, bundles, etc.).
     *
     * @return array An associative array of linked product types and their enabled status
     */
    public function getLinkedSettings(): array
    {
        return [
            LinkTypeEnum::VARIANTS->value          => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listVariantProducts),
            LinkTypeEnum::COLOR_VARIANTS->value    => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listColorVariantProducts),
            LinkTypeEnum::CAPACITY_VARIANTS->value => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listCapacityVariantProducts),
            LinkTypeEnum::ALTERNATE->value         => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listAlternateProducts),
            LinkTypeEnum::BUNDLED->value           => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listBundledProducts),
            LinkTypeEnum::BUNDLES->value           => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listBundles),
            LinkTypeEnum::RELATED->value           => $this->getBool(MergedPluginConfigKeyConstants::LIST_OPTION_listRelatedProducts),
        ];
    }


    /**
     * just for debugging - these settings steuern die Tabs in der Produkt-Detailansicht
     * 10/2024 created
     */
    public function getTabsSettings(): array
    {
        return [
            LinkTypeEnum::ALTERNATE->value         => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showAlternateProductsTab),
            LinkTypeEnum::BUNDLED->value           => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showBundledProductsTab),
            LinkTypeEnum::BUNDLES->value           => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showBundlesTab),
            LinkTypeEnum::CAPACITY_VARIANTS->value => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showCapacityVariantProductsTab),
            LinkTypeEnum::COLOR_VARIANTS->value    => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showColorVariantProductsTab),
            LinkTypeEnum::VARIANTS->value          => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showVariantProductsTab),
            LinkTypeEnum::RELATED->value           => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showRelatedProductsTab),
            LinkTypeEnum::SIMILAR->value           => $this->getBool(MergedPluginConfigKeyConstants::DISPLAY_OPTION_showSimilarProductsTab),
        ];
    }

}

================
File: src/Service/TopfeedHelperServiceV1.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Service;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Uuid\Uuid;

/**
 *
 * Helper class for managing product relationships and counts.
 *
 * This class provides utility methods for retrieving and counting
 * various types of product relationships such as alternate products,
 * bundled products, related products, and variants.
 *
 * 10/2024 made it a service (Helper --> TopfeedHelperServiceV1)
 * 06/2025 deprecated - use the single table version TopfeedHelperServiceV2
 *
 * @deprecated
 */
class TopfeedHelperServiceV1
{

    public function __construct(
        private readonly Connection $connection,
    )
    {
    }

    /**
     * Get query parameters for a specific product relationship type.
     *
     * @param string $key The type of product relationship
     * @return array|false An array of query parameters or false if the key is invalid
     */
    private static function _getQueryParams(string $key): array|false
    {
        if (in_array($key, ['alternate', 'bundled', 'related', 'similar', 'capacity_variant', 'color_variant', 'variant'])) {
            return [
                'table'  => 'topdata_product_to_' . $key,
                'field1' => $key . '_product_id',
                'field2' => 'product_id'
            ];
        }

        if ($key === 'bundles') {
            return [
                'table'  => 'topdata_product_to_bundled',
                'field1' => 'product_id',
                'field2' => 'bundled_product_id'
            ];
        }

        return false;
    }

    /**
     * Get linked product IDs for a specific product and relationship type.
     *
     * @param string $productId The ID of the product to get linked products for
     * @param string $link The type of product relationship
     * @return array An array of linked product IDs
     */
    private function _getLinkedIds(string $productId, string $link): array
    {
        $xids = [];
        if (!Uuid::isValid($productId)) {
            return $xids;
        }

        $queryParams = self::_getQueryParams($link);
        if (!$queryParams) {
            return $xids;
        }

        $productIds = $this->connection->executeQuery('
    SELECT LOWER(HEX(' . $queryParams['field1'] . ')) as id
     FROM ' . $queryParams['table'] . '
     WHERE 0x' . $productId . ' = ' . $queryParams['field2'] . '
            ')->fetchAllAssociative();
        foreach ($productIds as $id) {
            $xids[] = $id['id'];
        }
        return $xids;
    }

    /**
     * Get alternate product IDs for a given product.
     *
     * @param string $productId The ID of the product to get alternate products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of alternate product IDs
     */
    public function getAlternateProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'alternate');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'alternate');
        }

        return $xids;
    }


    /**
     * Get the count of alternate products for a given product.
     *
     * @param string $productId The ID of the product to count alternate products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of alternate products
     */
    public function getAlternateProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_alternate
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_alternate
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get bundled product IDs for a given product.
     *
     * @param string $productId The ID of the product to get bundled products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of bundled product IDs
     */
    public function getBundledProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'bundled');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'bundled');
        }

        return $xids;
    }


    /**
     * Get the count of bundled products for a given product.
     *
     * @param string $productId The ID of the product to count bundled products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of bundled products
     */
    public function getBundledProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_bundled
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_bundled
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get related product IDs for a given product.
     *
     * @param string $productId The ID of the product to get related products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of related product IDs
     */
    public function getRelatedProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'related');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'related');
        }

        return $xids;
    }


    /**
     * Get the count of related products for a given product.
     *
     * @param string $productId The ID of the product to count related products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of related products
     */
    public function getRelatedProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_related
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_related
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get bundle IDs for a given product.
     *
     * @param string $productId The ID of the product to get bundles for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of bundle IDs
     */
    public function getBundleIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'bundles');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'bundles');
        }

        return $xids;
    }


    /**
     * Get the count of bundles for a given product.
     *
     * @param string $productId The ID of the product to count bundles for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of bundles
     */
    public function getBundlesCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_bundled
 WHERE 0x' . $productId . ' = bundled_product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_bundled
     WHERE 0x' . $parentProductId . ' = bundled_product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get similar product IDs for a given product.
     *
     * @param string $productId The ID of the product to get similar products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of similar product IDs
     */
    public function getSimilarProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'similar');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'similar');
        }

        return $xids;
    }


    /**
     * Get the count of similar products for a given product.
     *
     * @param string $productId The ID of the product to count similar products for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of similar products
     */
    public function getSimilarProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_similar
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_similar
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get color variant product IDs for a given product.
     *
     * @param string $productId The ID of the product to get color variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of color variant product IDs
     */
    public function getColorVariantProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'color_variant');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'color_variant');
        }

        return $xids;
    }


    /**
     * Get the count of color variant products for a given product.
     *
     * @param string $productId The ID of the product to count color variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of color variant products
     */
    public function getColorVariantProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_color_variant
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_color_variant
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get capacity variant product IDs for a given product.
     *
     * @param string $productId The ID of the product to get capacity variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of capacity variant product IDs
     */
    public function getCapacityVariantProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'capacity_variant');

        if (!count($xids) && $parentProductId) {
            return $this->_getLinkedIds($parentProductId, 'capacity_variant');
        }

        return $xids;
    }


    /**
     * Get the count of capacity variant products for a given product.
     *
     * @param string $productId The ID of the product to count capacity variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of capacity variant products
     */
    public function getCapacityVariantProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_capacity_variant
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_capacity_variant
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }


    /**
     * Get variant product IDs for a given product.
     *
     * @param string $productId The ID of the product to get variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return array An array of variant product IDs
     */
    public function getVariantProductIds(string $productId, $parentProductId = null): array
    {
        $xids = $this->_getLinkedIds($productId, 'variant');

        if (!count($xids) && $parentProductId) {
            $xids = $this->_getLinkedIds($parentProductId, 'variant');
        }

        $xids = [];
        if (!Uuid::isValid($productId)) {
            return $xids;
        }

        return $xids;
    }


    /**
     * Get the count of variant products for a given product.
     *
     * @param string $productId The ID of the product to count variants for
     * @param string|null $parentProductId The ID of the parent product, if applicable
     * @return int The count of variant products
     */
    public function getVariantProductsCount(string $productId, $parentProductId = null): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $return = (int)$this->connection->executeQuery('
SELECT COUNT(*) as cnt
 FROM topdata_product_to_variant
 WHERE 0x' . $productId . ' = product_id
     LIMIT 1
            ')->fetchOne();

        if (!$return && $parentProductId && Uuid::isValid($parentProductId)) {
            return (int)$this->connection->executeQuery('
    SELECT COUNT(*) as cnt
     FROM topdata_product_to_variant
     WHERE 0x' . $parentProductId . ' = product_id
         LIMIT 1
                ')->fetchOne();
        }
        return $return;
    }

    /**
     * Get product IDs with other devices for a given product and its linked products.
     *
     * @param string $productId The ID of the main product
     * @param array $linkedIds An array of linked product IDs
     * @return array An array of product IDs with different device associations
     */
    public function getProductIdsWithOtherDevices(string $productId, array $linkedIds): array
    {
        $xids = [];
        $linkedIds[] = $productId;
        $linkedIds = '0x' . implode(',0x', $linkedIds);
        $datas = $this->connection->executeQuery('SELECT LOWER(HEX(product_id)) as product_id, LOWER(HEX(device_id)) as device_id FROM `topdata_device_to_product` WHERE product_id IN (' . $linkedIds . ') order by device_id')->fetchAllAssociative();
        $productIds = [];
        foreach ($datas as $data)
            $productIds[$data['product_id']][] = $data['device_id'];
        if (!isset($productIds[$productId])) {
            return [];
        }
        $org = $productIds[$productId];
        unset($productIds[$productId]);
        if ($productIds !== []) {
            foreach ($productIds as $id => $devices) {
                if (implode('|', $devices) !== implode('|', $org)) {
                    $xids[] = $id;
                }
            }
        }

        return $xids;
    }
}

================
File: src/Service/TopfeedHelperServiceV2.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Service;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Uuid\Uuid;
use Topdata\TopdataConnectorSW6\Enum\ProductRelationshipTypeEnumV2;


/**
 * Helper class for retrieving product relationship data from the unified relationships table.
 *
 * This service provides utility methods for retrieving and counting
 * various types of product relationships such as alternates, bundles, etc.
 * It operates on the single `topdata_product_relationships` table and uses the
 * ProductRelationshipTypeEnumV2 for type-safe internal operations.
 *
 * This class is backward-compatible with the public API of the previous multi-table version.
 */
class TopfeedHelperServiceV2
{
    private const RELATIONSHIPS_TABLE = 'topdata_product_relationships';

    public function __construct(
        private readonly Connection $connection,
    )
    {
    }

    // ========================================================================
    // PUBLIC API METHODS (Backward Compatible)
    // ========================================================================

    public function getAlternateProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::ALTERNATE, $productId, $parentProductId);
    }

    public function getAlternateProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::ALTERNATE, $productId, $parentProductId);
    }

    public function getBundledProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::BUNDLED, $productId, $parentProductId);
    }

    public function getBundledProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::BUNDLED, $productId, $parentProductId);
    }

    public function getRelatedProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::RELATED, $productId, $parentProductId);
    }

    public function getRelatedProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::RELATED, $productId, $parentProductId);
    }

    public function getBundleIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::BUNDLED, $productId, $parentProductId, 'reverse');
    }

    public function getBundlesCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::BUNDLED, $productId, $parentProductId, 'reverse');
    }

    public function getSimilarProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::SIMILAR, $productId, $parentProductId);
    }

    public function getSimilarProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::SIMILAR, $productId, $parentProductId);
    }

    public function getColorVariantProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::COLOR_VARIANT, $productId, $parentProductId);
    }

    public function getColorVariantProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::COLOR_VARIANT, $productId, $parentProductId);
    }

    public function getCapacityVariantProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::CAPACITY_VARIANT, $productId, $parentProductId);
    }

    public function getCapacityVariantProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::CAPACITY_VARIANT, $productId, $parentProductId);
    }

    public function getVariantProductIds(string $productId, ?string $parentProductId = null): array
    {
        return $this->getLinkedIds(ProductRelationshipTypeEnumV2::VARIANT, $productId, $parentProductId);
    }

    public function getVariantProductsCount(string $productId, ?string $parentProductId = null): int
    {
        return $this->getRelationshipCount(ProductRelationshipTypeEnumV2::VARIANT, $productId, $parentProductId);
    }

    // ========================================================================
    // PRIVATE "ENGINE" METHODS (Consolidated Logic)
    // ========================================================================

    /**
     * Generic engine to get all linked product IDs for a given product and relationship type.
     * Handles the fallback logic to a parent product.
     *
     * @param ProductRelationshipTypeEnumV2 $relationshipType The type of relationship to query.
     * @param 'forward'|'reverse' $lookupDirection 'forward' finds products linked *from* the ID, 'reverse' finds products linked *to* the ID.
     */
    private function getLinkedIds(ProductRelationshipTypeEnumV2 $relationshipType, string $productId, ?string $parentProductId, string $lookupDirection = 'forward'): array
    {
        if (!Uuid::isValid($productId)) {
            return [];
        }

        $ids = $this->fetchIdsForProduct($productId, $relationshipType, $lookupDirection);

        if (empty($ids) && $parentProductId && Uuid::isValid($parentProductId)) {
            return $this->fetchIdsForProduct($parentProductId, $relationshipType, $lookupDirection);
        }

        return $ids;
    }

    /**
     * Generic engine to count relationships for a given product and type.
     * Handles the fallback logic to a parent product.
     *
     * @param ProductRelationshipTypeEnumV2 $relationshipType The type of relationship to query.
     * @param 'forward'|'reverse' $lookupDirection 'forward' counts products linked *from* the ID, 'reverse' counts products linked *to* the ID.
     */
    private function getRelationshipCount(ProductRelationshipTypeEnumV2 $relationshipType, string $productId, ?string $parentProductId, string $lookupDirection = 'forward'): int
    {
        if (!Uuid::isValid($productId)) {
            return 0;
        }

        $count = $this->fetchCountForProduct($productId, $relationshipType, $lookupDirection);

        if ($count === 0 && $parentProductId && Uuid::isValid($parentProductId)) {
            return $this->fetchCountForProduct($parentProductId, $relationshipType, $lookupDirection);
        }

        return $count;
    }

    // ========================================================================
    // PRIVATE DATABASE HELPERS (Secure & Parameterized)
    // ========================================================================

    /**
     * Performs the actual secure database query to fetch linked IDs.
     *
     * @param 'forward'|'reverse' $lookupDirection
     */
    private function fetchIdsForProduct(string $productId, ProductRelationshipTypeEnumV2 $relationshipType, string $lookupDirection): array
    {
        $sourceColumn = ($lookupDirection === 'reverse') ? 'linked_product_id' : 'product_id';
        $targetColumn = ($lookupDirection === 'reverse') ? 'product_id' : 'linked_product_id';

        $ids = $this->connection->fetchFirstColumn(
            "SELECT LOWER(HEX(`{$targetColumn}`)) FROM `" . self::RELATIONSHIPS_TABLE . "` WHERE `{$sourceColumn}` = :productId AND `relationship_type` = :relationshipType",
            [
                'productId'        => Uuid::fromHexToBytes($productId),
                'relationshipType' => $relationshipType->value,
            ]
        );

        return $ids ?: [];
    }

    /**
     * Performs the actual secure database query to fetch the count.
     *
     * @param 'forward'|'reverse' $lookupDirection
     */
    private function fetchCountForProduct(string $productId, ProductRelationshipTypeEnumV2 $relationshipType, string $lookupDirection): int
    {
        $sourceColumn = ($lookupDirection === 'reverse') ? 'linked_product_id' : 'product_id';

        $count = $this->connection->fetchOne(
            "SELECT COUNT(*) FROM `" . self::RELATIONSHIPS_TABLE . "` WHERE `{$sourceColumn}` = :productId AND `relationship_type` = :relationshipType",
            [
                'productId'        => Uuid::fromHexToBytes($productId),
                'relationshipType' => $relationshipType->value,
            ]
        );

        return (int)$count;
    }

    /**
     * Get product IDs with other devices for a given product and its linked products.
     * This method has been updated to use secure parameterized queries.
     */
    public function getProductIdsWithOtherDevices(string $productId, array $linkedIds): array
    {
        $allProductIds = array_filter(array_unique([...$linkedIds, $productId]), fn($id) => Uuid::isValid($id));

        if (empty($allProductIds)) {
            return [];
        }

        $deviceMappings = $this->connection->fetchAllAssociative(
            'SELECT LOWER(HEX(product_id)) as product_id, LOWER(HEX(device_id)) as device_id 
             FROM `topdata_device_to_product` 
             WHERE product_id IN (:ids) 
             ORDER BY device_id',
            ['ids' => $allProductIds],
            ['ids' => Connection::PARAM_STR_ARRAY]
        );

        $productDevices = [];
        foreach ($deviceMappings as $mapping) {
            $productDevices[$mapping['product_id']][] = $mapping['device_id'];
        }

        if (!isset($productDevices[$productId])) {
            return [];
        }

        $originalDeviceSignature = implode('|', $productDevices[$productId]);
        unset($productDevices[$productId]);

        $differentDeviceIds = [];
        foreach ($productDevices as $id => $devices) {
            if (implode('|', $devices) !== $originalDeviceSignature) {
                $differentDeviceIds[] = $id;
            }
        }

        return $differentDeviceIds;
    }
}

================
File: src/Storefront/Page/CompatibleDevicesWidget/CompatibleDevicesWidgetPageV1.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\CompatibleDevicesWidget;

use Shopware\Storefront\Page\Page;

/**
 * 04/2025 FIXME: this is code duplication with CompatibleDevicesWidgetPage from TopdataTopFinderProSW6
 */
class CompatibleDevicesWidgetPageV1 extends Page
{
    protected array $devices = [];
    public array $uniqueSeries = [];
    public array $uniqueTypes = [];
    public ?\Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity $product = null;
    public int $noTypeDevicesCount = 0;
    
    public function getDevices() : array
    {
        return $this->devices;
    }
    
    public function setDevices(array $devices): void
    {
        $this->devices = $devices;
        foreach ($devices as $device) {
            if($device['series_id'] && !isset($this->uniqueSeries[$device['series_id']])) {
                $this->uniqueSeries[$device['series_id']] = $device['series_name'];
            }
            if($device['type_id'] && !isset($this->uniqueTypes[$device['type_id']])) {
                $this->uniqueTypes[$device['type_id']] = $device['type_name'];
            }
            elseif(!$device['type_id']) {
                $this->noTypeDevicesCount++;
            }
            $this->uniqueTypes = array_unique($this->uniqueTypes);
            sort($this->uniqueTypes, SORT_LOCALE_STRING);
        }
    }
}

================
File: src/Storefront/Page/CompatibleDevicesWidget/CompatibleDevicesWidgetPageV1Loader.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\CompatibleDevicesWidget;

use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\GenericPageLoader;
use Symfony\Component\HttpFoundation\Request;
use Topdata\TopdataTopFeedSW6\Service\SettingsService;
use Doctrine\DBAL\Connection;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;

/**
 * 04/2025 FIXME: this is code duplication with CompatibleDevicesWidgetPageLoader from TopdataTopFinderProSW6
 */
class CompatibleDevicesWidgetPageV1Loader
{

    public function __construct(
        private readonly GenericPageLoader      $genericLoader,
        private readonly Connection             $connection,
        private readonly SalesChannelRepository $productRepository,
    )
    {
    }

    public function load(Request $request, SalesChannelContext $salesChannelContext, string $productid): CompatibleDevicesWidgetPageV1
    {
        /** @var CompatibleDevicesWidgetPageV1 $page */
        $page = CompatibleDevicesWidgetPageV1::createFrom(
            $this->genericLoader->load($request, $salesChannelContext)
        );
        $page->product = $this->searchProduct($productid, $salesChannelContext);
        $page->setDevices($this->searchDevices($productid));

        return $page;
    }

    private function searchProduct(string $productid, SalesChannelContext $salesChannelContext): ?SalesChannelProductEntity
    {
        return $this
            ->productRepository
            ->search(
                (new Criteria([$productid]))->addAssociation('manufacturer'),
                $salesChannelContext
            )
            ->getEntities()
            ->first();
    }

    private function searchDevices(string $productid): array
    {
        $devices = $this->connection->fetchAllAssociative('
SELECT topdata_device.model as name, 
       LOWER(HEX(topdata_device.id)) as id, 
       LOWER(HEX(topdata_device.brand_id)) as brand_id, 
       LOWER(HEX(topdata_device.series_id)) as series_id, 
       LOWER(HEX(topdata_device.type_id)) as type_id, 
       topdata_device.code as code, 
       topdata_brand.label as brand_name,
       topdata_series.label as series_name,
       topdata_device_type.label as type_name
  FROM topdata_device_to_product, 
       topdata_brand,
       topdata_device
  LEFT JOIN `topdata_series` ON `topdata_device`.series_id = `topdata_series`.`id`
  LEFT JOIN topdata_device_type ON `topdata_device`.`type_id` = `topdata_device_type`.`id`
  WHERE (topdata_device.is_enabled=1)
        AND(topdata_device_to_product.product_id=0x' . $productid . ')
        AND(topdata_device_to_product.device_id = topdata_device.id)
        AND(topdata_brand.id=topdata_device.brand_id)
  ORDER BY topdata_brand.label, topdata_series.label, topdata_device.model
            ');

        return $devices;
    }
}

================
File: src/Storefront/Page/LinkedProductsPopup/LinkedProductsPopupLoadedEvent.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup;

use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\PageLoadedEvent;
use Symfony\Component\HttpFoundation\Request;

class LinkedProductsPopupLoadedEvent extends PageLoadedEvent
{
    public function __construct(protected \Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkedProductsPopupPage $page, SalesChannelContext $salesChannelContext, Request $request)
    {
        parent::__construct($salesChannelContext, $request);
    }

    public function getPage(): LinkedProductsPopupPage
    {
        return $this->page;
    }
}

================
File: src/Storefront/Page/LinkedProductsPopup/LinkedProductsPopupLoader.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup;

use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\Adapter\Translation\Translator;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntityCollection;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\GenericPageLoader;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Topdata\TopdataTopFeedSW6\Content\Product\Events\AssociatedProductsCriteriaEvent;

/**
 * Class LinkedProductsPopupLoader
 * 
 * This class is responsible for loading and preparing data for the LinkedProductsPopupPage.
 * It handles the retrieval of product information, associated products, and prepares
 * the page data for display in the popup.
 */
class LinkedProductsPopupLoader
{
    public function __construct(
        private readonly GenericPageLoader        $genericPageLoader,
        private readonly SalesChannelRepository   $productRepository,
        private readonly EventDispatcherInterface $eventDispatcher,
        private readonly Translator               $translator,
        private readonly ProductIdRetriever       $productIdRetriever,
    )
    {
    }

    /**
     * ==== MAIN ====
     * 10/2024 renamed from load() to buildLinkedProductsPopupPage()
     *
     * it creates a LinkedProductsPopupPage object and fills it with data
     *
     * @param Request $request The current HTTP request
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @param string $productId The ID of the product to load
     * @param LinkTypeEnum $pageTypeEnum The type of linked products to retrieve
     * @return LinkedProductsPopupPage The prepared LinkedProductsPopup page object
     */
    public function buildLinkedProductsPopupPage(Request $request, SalesChannelContext $salesChannelContext, string $productId, LinkTypeEnum $pageTypeEnum): LinkedProductsPopupPage
    {
        $page = LinkedProductsPopupPage::createFrom($this->genericPageLoader->load($request, $salesChannelContext));

        $product = $this->_getProduct($productId, $salesChannelContext);
        if (!$product) {
            $page->pageTitle = $this->translator->trans('topdata-topfeed.product.notFound');
            return $page;
        }

        $page->product = $product;
        $page->pageTitle = $this->_getPageTitle($pageTypeEnum, $product);

        $productIds = $this->productIdRetriever->getLinkedProductIdsByLinkType($pageTypeEnum, $productId, $product->getParentId());
        $page->products = $productIds ? $this->_loadProducts($productIds, $salesChannelContext) : null;

        $this->eventDispatcher->dispatch(
            new LinkedProductsPopupLoadedEvent($page, $salesChannelContext, $request)
        );

        return $page;
    }

    /**
     * Retrieves a single product by its ID.
     *
     * @param string $productId The ID of the product to retrieve
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return SalesChannelProductEntity|null The retrieved product entity or null if not found
     */
    private function _getProduct(string $productId, SalesChannelContext $salesChannelContext): ?SalesChannelProductEntity
    {
        return $this->productRepository
            ->search(new Criteria([$productId]), $salesChannelContext)
            ->get($productId);
    }

    /**
     * Generates the page title based on the page type and product.
     *
     * @param LinkTypeEnum $pageType The type of linked products page
     * @param SalesChannelProductEntity $product The main product entity
     * @return string The generated page title
     */
    private function _getPageTitle(LinkTypeEnum $pageType, SalesChannelProductEntity $product): string
    {
        $titleKey = 'topdata-topfeed.product.' . $pageType->value;
        return sprintf(
            '%s %s "%s"',
            $this->translator->trans($titleKey),
            $this->translator->trans('topdata-topfeed.for'),
            $product->getName()
        );
    }

    /**
     * Loads multiple products by their IDs.
     *
     * This method retrieves multiple products, applies filters, and adds necessary
     * associations for display in the popup.
     *
     * @param array $productIds An array of product IDs to load
     * @param SalesChannelContext $salesChannelContext The current sales channel context
     * @return EntityCollection<SalesChannelProductEntity> A collection of loaded product entities
     */
    private function _loadProducts(array $productIds, SalesChannelContext $salesChannelContext): EntityCollection
    {
        $criteria = (new Criteria($productIds))
            ->addFilter(new ProductAvailableFilter($salesChannelContext->getSalesChannel()->getId(), ProductVisibilityDefinition::VISIBILITY_LINK))
            ->addAssociation('prices')
            ->addAssociation('cover')
            ->addAssociation('manufacturer');

        $this->eventDispatcher->dispatch(
            new AssociatedProductsCriteriaEvent($criteria, $salesChannelContext)
        );

        return $this->productRepository->search($criteria, $salesChannelContext)->getEntities();
    }
}

================
File: src/Storefront/Page/LinkedProductsPopup/LinkedProductsPopupPage.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup;

use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntityCollection;
use Shopware\Storefront\Page\Page;

class LinkedProductsPopupPage extends Page
{
    /**
     * @var EntityCollection<SalesChannelProductEntity>|null
     */
    public ?EntityCollection $products = null;

    public string $pageTitle = '';

    public ?SalesChannelProductEntity $product = null;
}

================
File: src/Storefront/Page/LinkedProductsPopup/LinkTypeEnum.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup;

/**
 * NOTE: the values of the enum are used as keys for translations, so better not change them
 *
 * 10/2024 created
 */
enum LinkTypeEnum: string
{
    case ALTERNATE         = 'alternate';
    case BUNDLED           = 'bundled';
    case BUNDLES           = 'bundles';
    case RELATED           = 'related';
    case SIMILAR           = 'similar';
    case OTHER_COLORS      = 'colors';
    case COLOR_VARIANTS    = 'color';
    case CAPACITY_VARIANTS = 'capacity';
    case VARIANTS          = 'variants';
}

================
File: src/Storefront/Page/LinkedProductsPopup/ProductIdRetriever.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup;

use Doctrine\DBAL\Connection;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV1;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV2;
use Topdata\TopdataTopFeedSW6\Service\SettingsService;

/**
 * 10/2024 created
 */
class ProductIdRetriever
{

    public function __construct(
        private readonly SettingsService      $settingsService,
        private readonly TopfeedHelperServiceV1 $topfeedHelperServiceV2,
    )
    {
    }

//    /**
//     * 10/2024 the original version before getting merged with getLinkedProductIdsByLinkType
//     */
//    public function getProductIds(LinkTypeEnum $pageType, string $productId, ?string $parentId): array
//    {
//        return match ($pageType) {
//            LinkTypeEnum::ALTERNATE         => $this->topfeedHelperServiceV2->getAlternateProductIds($productId, $parentId),
//            LinkTypeEnum::BUNDLED           => $this->topfeedHelperServiceV2->getBundledProductIds($productId, $parentId),
//            LinkTypeEnum::BUNDLES           => $this->topfeedHelperServiceV2->getBundleIds($productId, $parentId),
//            LinkTypeEnum::RELATED           => $this->topfeedHelperServiceV2->getRelatedProductIds($productId, $parentId),
//            LinkTypeEnum::COLOR_VARIANTS    => $this->topfeedHelperServiceV2->getColorVariantProductIds($productId, $parentId),
//            LinkTypeEnum::CAPACITY_VARIANTS => $this->topfeedHelperServiceV2->getCapacityVariantProductIds($productId, $parentId),
//            LinkTypeEnum::VARIANTS          => $this->topfeedHelperServiceV2->getVariantProductIds($productId, $parentId),
//            LinkTypeEnum::SIMILAR           => $this->topfeedHelperServiceV2->getSimilarProductIds($productId, $parentId),
//            default                         => [],
//        };
//    }
//
//
//    /**
//     * 10/2024 the original version before getting merged with getProductIds
//     * FIXME: it is same like getProductIds, but it is used in TopdataTopFeedApiController?
//     *
//     * 10/2024 moved from TopdataTopFeedApiController to here
//     *
//     * Retrieves linked product IDs based on the link type and main product ID.
//     *
//     * This private method is used internally to fetch the IDs of linked products
//     * for a specific link type (e.g., variants, alternates, bundles) and main product.
//     *
//     * @param LinkTypeEnum $linkTypeEnum The type of link
//     * @param string $mainProductId The ID of the main product
//     * @return array An array of linked product IDs
//     */
//    public function getLinkedProductIdsByLinkType(LinkTypeEnum $linkTypeEnum, string $mainProductId): array
//    {
//        return match ($linkTypeEnum) {
//            LinkTypeEnum::VARIANTS          => $this->topfeedHelperServiceV2->getVariantProductIds($mainProductId),
//            LinkTypeEnum::COLOR_VARIANTS    => $this->topfeedHelperServiceV2->getColorVariantProductIds($mainProductId),
//            LinkTypeEnum::CAPACITY_VARIANTS => $this->topfeedHelperServiceV2->getCapacityVariantProductIds($mainProductId),
//            LinkTypeEnum::ALTERNATE         => $this->topfeedHelperServiceV2->getAlternateProductIds($mainProductId),
//            LinkTypeEnum::BUNDLED           => $this->topfeedHelperServiceV2->getBundledProductIds($mainProductId),
//            LinkTypeEnum::BUNDLES           => $this->topfeedHelperServiceV2->getBundleIds($mainProductId),
//            LinkTypeEnum::RELATED           => $this->topfeedHelperServiceV2->getRelatedProductIds($mainProductId),
//            default                         => [],
//        };
//    }

    /**
     * 10/2024 created
     */
    public function getLinkedProductIdsByLinkType(LinkTypeEnum $linkType, string $productId, ?string $parentId = null): array
    {
        return match ($linkType) {
            LinkTypeEnum::ALTERNATE         => $this->topfeedHelperServiceV2->getAlternateProductIds($productId, $parentId),
            LinkTypeEnum::BUNDLED           => $this->topfeedHelperServiceV2->getBundledProductIds($productId, $parentId),
            LinkTypeEnum::BUNDLES           => $this->topfeedHelperServiceV2->getBundleIds($productId, $parentId),
            LinkTypeEnum::RELATED           => $this->topfeedHelperServiceV2->getRelatedProductIds($productId, $parentId),
            LinkTypeEnum::COLOR_VARIANTS    => $this->topfeedHelperServiceV2->getColorVariantProductIds($productId, $parentId),
            LinkTypeEnum::CAPACITY_VARIANTS => $this->topfeedHelperServiceV2->getCapacityVariantProductIds($productId, $parentId),
            LinkTypeEnum::VARIANTS          => $this->topfeedHelperServiceV2->getVariantProductIds($productId, $parentId),
            LinkTypeEnum::SIMILAR           => $this->topfeedHelperServiceV2->getSimilarProductIds($productId, $parentId),
            default                         => [],
        };
    }

    /**
     * 10/2024 created (extracted from TopFeedApiController)
     */
    public function getProductIdsUsingSettings(LinkTypeEnum $linkType, string $productId): array
    {
        return match ($linkType) {
            LinkTypeEnum::VARIANTS          => $this->settingsService->getBool('listVariantProducts') ? $this->topfeedHelperServiceV2->getVariantProductIds($productId) : [],
            LinkTypeEnum::COLOR_VARIANTS    => $this->settingsService->getBool('listColorVariantProducts') ? $this->topfeedHelperServiceV2->getColorVariantProductIds($productId) : [],
            LinkTypeEnum::CAPACITY_VARIANTS => $this->settingsService->getBool('listCapacityVariantProducts') ? $this->topfeedHelperServiceV2->getCapacityVariantProductIds($productId) : [],
            LinkTypeEnum::ALTERNATE         => $this->settingsService->getBool('listAlternateProducts') ? $this->topfeedHelperServiceV2->getAlternateProductIds($productId) : [],
            LinkTypeEnum::BUNDLED           => $this->settingsService->getBool('listBundledProducts') ? $this->topfeedHelperServiceV2->getBundledProductIds($productId) : [],
            LinkTypeEnum::BUNDLES           => $this->settingsService->getBool('listBundles') ? $this->topfeedHelperServiceV2->getBundleIds($productId) : [],
            LinkTypeEnum::RELATED           => $this->settingsService->getBool('listRelatedProducts') ? $this->topfeedHelperServiceV2->getRelatedProductIds($productId) : [],
            default                         => [],
        };
    }


}

================
File: src/Subscriber/ProductMenuLoadedEventSubscriber.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Topdata\TopdataProductMenu\Storefront\Page\ProductMenu\ProductMenuLoadedEvent;
use Shopware\Core\Framework\Adapter\Translation\Translator;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV1;
use Topdata\TopdataTopFeedSW6\Service\TopfeedHelperServiceV2;
use Doctrine\DBAL\Connection;
use Topdata\TopdataTopFeedSW6\Storefront\Page\LinkedProductsPopup\LinkTypeEnum;
use Topdata\TopdataTopFeedSW6\Service\SettingsService;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Shopware\Core\Framework\Uuid\Uuid;

/**
 * NOTE: ONLY RELEVANT IF THE PLUGIN topdata-product-listing-menu-sw6 IS INSTALLED
 *
 * This subscriber is responsible for adding menu items to the product menu
 * based on various product relationships such as variants, alternates, bundles, etc.
 * It listens to the ProductMenuLoadedEvent and modifies the menu accordingly.
 */
class ProductMenuLoadedEventSubscriber implements EventSubscriberInterface
{


    public function __construct(
        private readonly Translator            $translator,
        private readonly Connection            $connection,
        private readonly SettingsService       $settingsService,
        private readonly UrlGeneratorInterface $router,
        private readonly TopfeedHelperServiceV1  $topfeedHelperServiceV2,
    )
    {
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array The event names to listen to
     */
    public static function getSubscribedEvents(): array
    {
        return [
            ProductMenuLoadedEvent::class => 'addMenuItems',
        ];
    }

    /**
     * Adds menu items to the product menu based on various product relationships.
     *
     * This method checks for different types of related products (variants, alternates, bundles, etc.)
     * and adds corresponding menu items if they exist and are enabled in the settings.
     *
     * @param ProductMenuLoadedEvent $event The event containing the product menu
     */
    public function addMenuItems(ProductMenuLoadedEvent $event): void
    {
        $productId = $event->getPage()->getProductId();
        if (!Uuid::isValid($productId)) {
            return;
        }
        $parentId = $this->connection->executeQuery('
                SELECT LOWER(HEX(parent_id)) as parentId FROM `product`
                 WHERE id = 0x' . $productId . '
                 LIMIT 1')->fetchOne();
        $buttonClasses = ['topdata-top-feed-linked-products'];

        if (!($this->settingsService->getBool('menuHideForVariated') && $parentId)) {
            if ($this->settingsService->getBool('showColorVariantProducts')) {
                $colorVariantsCount = $this->topfeedHelperServiceV2->getColorVariantProductsCount($productId, $parentId);
                if ($colorVariantsCount !== 0) {
                    $event->getPage()->setMenuItem(
                        $this->translator->trans('topdata-topfeed.menu.colors') . " ($colorVariantsCount)",
                        '#',
                        [
                            'path' => $this->router->generate(
                                'frontend.topdata_top_feed.products',
                                [
                                    'productId' => $productId,
                                    'linkType'  => LinkTypeEnum::COLOR_VARIANTS->value
                                ]
                            ),
                        ],
                        $buttonClasses
                    );
                }
            }

            if ($this->settingsService->getBool('showCapacityVariantProducts')) {
                $capacityVariantsCount = $this->topfeedHelperServiceV2->getCapacityVariantProductsCount($productId, $parentId);
                if ($capacityVariantsCount !== 0) {
                    $event->getPage()->setMenuItem(
                        $this->translator->trans('topdata-topfeed.menu.capacity') . " ($capacityVariantsCount)",
                        '#',
                        [
                            'path' => $this->router->generate(
                                'frontend.topdata_top_feed.products',
                                [
                                    'productId' => $productId,
                                    'linkType'  => LinkTypeEnum::CAPACITY_VARIANTS->value
                                ]
                            ),
                        ],
                        $buttonClasses
                    );
                }
            }
        }

        if ($this->settingsService->getBool('showVariantProducts')) {
            $variantsCount = $this->topfeedHelperServiceV2->getVariantProductsCount($productId, $parentId);
            if ($variantsCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.variants') . " ($variantsCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::VARIANTS->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }

        if ($this->settingsService->getBool('showAlternateProducts')) {
            $alternateCount = $this->topfeedHelperServiceV2->getAlternateProductsCount($productId, $parentId);
            if ($alternateCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.alternate') . " ($alternateCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::ALTERNATE->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }

        if ($this->settingsService->getBool('showBundledProducts')) {
            $bundledCount = $this->topfeedHelperServiceV2->getBundledProductsCount($productId, $parentId);
            if ($bundledCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.bundled') . " ($bundledCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::BUNDLED->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }

        if ($this->settingsService->getBool('showBundles')) {
            $bundlesCount = $this->topfeedHelperServiceV2->getBundlesCount($productId, $parentId);
            if ($bundlesCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.bundles') . " ($bundlesCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::BUNDLES->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }

        if ($this->settingsService->getBool('showRelatedProducts')) {
            $relatedCount = $this->topfeedHelperServiceV2->getRelatedProductsCount($productId, $parentId);
            if ($relatedCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.related') . " ($relatedCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::RELATED->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }

        if ($this->settingsService->getBool('showSimilarProducts')) {
            $similarProductsCount = $this->topfeedHelperServiceV2->getSimilarProductsCount($productId, $parentId);
            if ($similarProductsCount !== 0) {
                $event->getPage()->setMenuItem(
                    $this->translator->trans('topdata-topfeed.menu.similar') . " ($similarProductsCount)",
                    '#',
                    [
                        'path' => $this->router->generate(
                            'frontend.topdata_top_feed.products',
                            [
                                'productId' => $productId,
                                'linkType'  => LinkTypeEnum::SIMILAR->value
                            ]
                        ),
                    ],
                    $buttonClasses
                );
            }
        }
    }
}

================
File: src/Subscriber/ProductPageCriteriaSubscriber.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Subscriber;

// use Shopware\Storefront\Page\Product\ProductLoaderCriteriaEvent;
//we also deprecated the \Shopware\Storefront\Page\Product\ProductLoaderCriteriaEvent.
//If you have subscribed to this event to extend the product detail page, replace the event with \Shopware\Storefront\Page\Product\ProductPageCriteriaEvent
//If you have subscribed to this event to extend the listing quick view, replace the event with \Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageCriteriaEvent
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Topdata\TopdataTopFeedSW6\Service\SettingsService;
use TopdataSoftwareGmbH\Util\UtilDebug;

/**
 * FIXME: it does actually nothing (commented out implementation) - remove this?
 *        seems like this got replaced by ProductPageLoadedEventSubscriber
 *
 * 10/2024 replaced removed ProductLoaderCriteriaEvent with ProductPageCriteriaEvent
 *
 * This subscriber is responsible for adding associations to the product page criteria
 * based on the plugin configuration.
 * It listens to the ProductPageCriteriaEvent and modifies the criteria accordingly.
 */
class ProductPageCriteriaSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly SystemConfigService $systemConfigService,
        private readonly SettingsService $settingsService,
    )
    {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ProductPageCriteriaEvent::class => 'onProductCriteriaLoaded',
        ];
    }

    public function onProductCriteriaLoaded(ProductPageCriteriaEvent $event): void
    {
//        UtilDebug::dd($this->settingsService->getTopConfigFlat());

//        $config = $this->systemConfigService->get('TopdataTopFeedSW6.config');
//        if(isset($config['showAlternateProductsTab']) && $config['showAlternateProductsTab']) {
//            $event->getCriteria()->addAssociation('alternate_products');
//        }
//        if(isset($config['showBundledProductsTab']) && $config['showBundledProductsTab']) {
//            $event->getCriteria()->addAssociation('bundled_products');
//        }
//        if(isset($config['showRelatedProductsTab']) && $config['showRelatedProductsTab']) {
//            $event->getCriteria()->addAssociation('related_products');
//        }
//        if(isset($config['showSimilarProductsTab']) && $config['showSimilarProductsTab']) {
//            $event->getCriteria()->addAssociation('similar_products');
//        }

//        $event->getCriteria()->addAssociation('topdata.product');
    }
}

================
File: src/Subscriber/ProductPageLoadedEventSubscriber.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6\Subscriber;

use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Topdata\TopdataTopFeedSW6\Service\ProductPageExtender;

/**
 * Class ProductPageLoadedEventSubscriber
 *
 * This subscriber handles the ProductPageLoadedEvent to add additional product information
 * such as associated products, variants, and devices to the product page.
 */
class ProductPageLoadedEventSubscriber implements EventSubscriberInterface
{


    public function __construct(
        private readonly ProductPageExtender    $productPageExtender,
    )
    {
    }

    /**
     * Returns an array of events this subscriber wants to listen to.
     *
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
        return [
            ProductPageLoadedEvent::class => 'handleProductPageLoadedEvent',
        ];
    }

    /**
     * ==== MAIN ====
     *
     * Handles the ProductPageLoadedEvent by adding additional product information.
     *
     * @param ProductPageLoadedEvent $event
     */
    public function handleProductPageLoadedEvent(ProductPageLoadedEvent $event): void
    {
        $this->productPageExtender->init($event->getPage(), $event->getSalesChannelContext());
        $this->productPageExtender->addAssociatedProducts();
        $this->productPageExtender->addProductVariants();
        $this->productPageExtender->addProductHasDevices();
        $this->productPageExtender->addProductVariantOtherDevices();
    }


}

================
File: src/TopdataTopFeedSW6.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataTopFeedSW6;

use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Topdata\TopdataFoundationSW6\DependencyInjection\TopConfigRegistryCompilerPass;

class TopdataTopFeedSW6 extends Plugin
{

    private const MAPPINGS = [
        'productName'                    => 'import.product.name',
        'productDescription'             => 'import.product.description',
        'productBrand'                   => 'import.product.brand',
        'productEan'                     => 'import.product.ean',
        'productOem'                     => 'import.product.mpn',
        'productImages'                  => 'import.product.images.enabled',
        'productImagesDelete'            => 'import.product.images.deleteUnused', // UNUSED
        'productSpecifications'          => 'import.product.specifications',
        'specReferencePCD'               => 'import.product.references.pcd',
        'specReferenceOEM'               => 'import.product.references.oem',
        'productWaregroups'              => 'import.categories.enabled',
        'productWaregroupsDelete'        => 'import.categories.clearBeforeImport',
        'productWaregroupsParent'        => 'import.categories.parentId',
        'productSimilar'                 => 'crossSelling.similar.import',
        'productSimilarCross'            => 'crossSelling.similar.enabled',
        'productAlternate'               => 'crossSelling.alternate.import',
        'productAlternateCross'          => 'crossSelling.alternate.enabled',
        'productRelated'                 => 'crossSelling.related.import',
        'productRelatedCross'            => 'crossSelling.related.enabled',
        'productBundled'                 => 'crossSelling.bundled.import',
        'productBundledCross'            => 'crossSelling.bundled.enabled',
        'productVariant'                 => 'crossSelling.variant.import',
        'productVariantCross'            => 'crossSelling.variant.enabled',
        'productVariantColor'            => 'crossSelling.variantColor.import',
        'productVariantColorCross'       => 'crossSelling.variantColor.enabled',
        'productVariantCapacity'         => 'crossSelling.variantCapacity.import',
        'productVariantCapacityCross'    => 'crossSelling.variantCapacity.enabled',
        'colorVariants'                  => 'detail.variants.color.display',
        'capacityVariants'               => 'detail.variants.capacity.display',
        'variantsHideForVariated'        => 'detail.variants.hideForVariated',
        'showProductDevicesTab'          => 'detail.tabs.compatibleDevices',
        'showVariantProductsTab'         => 'detail.tabs.variants',
        'showColorVariantProductsTab'    => 'detail.tabs.variantColors',
        'showCapacityVariantProductsTab' => 'detail.tabs.variantCapacities',
        'showAlternateProductsTab'       => 'detail.tabs.alternateProducts',
        'showBundledProductsTab'         => 'detail.tabs.bundledProducts',
        'showBundlesTab'                 => 'detail.tabs.bundles',
        'showRelatedProductsTab'         => 'detail.tabs.relatedProducts',
        'showSimilarProductsTab'         => 'detail.tabs.similarProducts',
        'showVariantProducts'            => 'menu.variants',
        'showColorVariantProducts'       => 'menu.variantColors',
        'showCapacityVariantProducts'    => 'menu.variantCapacities',
        'showAlternateProducts'          => 'menu.alternateProducts',
        'showBundledProducts'            => 'menu.bundledProducts',
        'showBundles'                    => 'menu.bundles',
        'showRelatedProducts'            => 'menu.relatedProducts',
        'showSimilarProducts'            => 'menu.similarProducts',
        'menuHideForVariated'            => 'menu.hideForVariated',
        'listVariantsLimit'              => 'list.variantsLimit',
        'listVariantProducts'            => 'list.variants',
        'listColorVariantProducts'       => 'list.variantColors',
        'listCapacityVariantProducts'    => 'list.variantCapacities',
        'listAlternateProducts'          => 'list.alternateProducts',
        'listBundledProducts'            => 'list.bundledProducts',
        'listBundles'                    => 'list.bundles',
        'listRelatedProducts'            => 'list.relatedProducts',
        'listHideForVariated'            => 'list.hideForVariated'
    ];

    public function build(ContainerBuilder $container): void
    {
        parent::build($container);

        // ---- register the plugin in Topdata Configration Center's TopConfigRegistry
        if(class_exists(TopConfigRegistryCompilerPass::class)) {
            $container->addCompilerPass(new TopConfigRegistryCompilerPass(__CLASS__, self::MAPPINGS));
        }
    }
    
    
    public function uninstall(UninstallContext $context): void
    {
        parent::uninstall($context);

        if ($context->keepUserData()) {
            return;
        }
        
    }
    
}

================
File: .gitignore
================
# Add any directories, files, or patterns you don't want to be tracked by version control
.idea
dist/
.aider*

.php-cs-fixer.cache

.rector-cache

vendor/
composer.lock
builds/

src/Resources/public/administration/.vite/
src/Resources/public/administration/assets/

repomix-output.txt

node_modules

================
File: .php-cs-fixer.dist.php
================
<?php

// .php-cs-fixer.dist.php - PHP CS Fixer configuration file
// 2024-06-26 created
// This file defines the coding standards and rules for the project.

use PhpCsFixer\Config;
use PhpCsFixer\Finder;

$finder = Finder::create()
    // ->in(__DIR__)
    ->in(__DIR__ . '/src')
    ->name('*.php')
    ->exclude('vendor')
    ->exclude('var');

$config = new Config();
return $config
    ->setRules([
        '@PSR12' => true,
        'array_syntax' => ['syntax' => 'short'],
        'binary_operator_spaces' => [
            'default' => 'align_single_space_minimal',
        ],
        'blank_line_after_namespace' => true,
        'blank_line_after_opening_tag' => true,
        'blank_line_before_statement' => [
            'statements' => ['return'],
        ],
        'braces' => [
            'position_after_functions_and_oop_constructs' => 'next',
        ],
        'cast_spaces' => ['space' => 'single'],
        'concat_space' => ['spacing' => 'one'],
        'declare_equal_normalize' => ['space' => 'none'],
        'function_typehint_space' => true,
        'include' => true,
        'lowercase_cast' => true,
        'no_extra_blank_lines' => [
            'tokens' => [
                'extra',
                'throw',
                'use',
            ],
        ],
        'no_trailing_whitespace' => true,
        'no_whitespace_before_comma_in_array' => true,
        'single_quote' => true,
        'ternary_operator_spaces' => true,
        'trailing_comma_in_multiline' => ['elements' => ['arrays']],
        'trim_array_spaces' => true,
        'unary_operator_spaces' => true,
    ])
    ->setFinder($finder);

================
File: composer.json
================
{
    "name": "topdata/topdata-top-feed-sw6",
    "description": "Synchronizes extended product data, cross-selling relationships, and compatible devices from the TopData web service to enhance product pages.",
    "version": "8.0.0",
    "type": "shopware-platform-plugin",
    "license": "MIT",
    "authors": [
        {
            "name": "TopData Software GmbH",
            "homepage": "https://www.topdata.de",
            "role": "Manufacturer"
        }
    ],
    "require": {
        "php": "^8.2",
        "shopware/core": "6.7.*",
        "shopware/storefront": "6.7.*",
        "topdata/topdata-connector-sw6": ">=8.0.0",
        "topdata/topdata-foundation-sw6": ">=1.3.0"
    },
    "extra": {
        "shopware-plugin-class": "Topdata\\TopdataTopFeedSW6\\TopdataTopFeedSW6",
        "label": {
            "de-DE": "Topdata TopFeed",
            "en-GB": "Topdata TopFeed"
        },
        "description": {
            "de-DE": "Mit dem Topdata TopFeed-Plugin können Sie Produktinformationen, die in der Cloud gespeichert sind, automatisch aktualisieren. Der TopdataConnector wird benötigt, um dieses Plugin zu betreiben.",
            "en-GB": "The Topdata TopFeed plugin allows you automatically update products information, stored on cloud. The TopdataConnector is needed to work this plugin."
        },
        "manufacturerLink": {
            "de-DE": "https://www.topdata.de",
            "en-GB": "https://www.topdata.de"
        },
        "supportLink": {
            "de-DE": "https://www.topdata.de/topfeed",
            "en-GB": "https://www.topdata.de/topfeed"
        }
    },
    "autoload": {
        "psr-4": {
            "Topdata\\TopdataTopFeedSW6\\": "src/"
        }
    }
}

================
File: package.json
================
{
  "name": "topdata-topfeed-sw6",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "vite": "^2.0.0",
    "@vitejs/plugin-vue": "^1.0.0"
  }
}

================
File: php-cs-fixer.md
================
# PHP CS Fixer Usage Instructions

PHP CS Fixer is a tool for automatically fixing PHP coding standards issues in your code. This document provides basic instructions on how to use PHP CS Fixer in this project.

## Running PHP CS Fixer

1. To perform a dry run (see what would be fixed without actually changing files):
   ```
   php php-cs-fixer.phar fix --dry-run
   ```

2. To fix files:
   ```
   php php-cs-fixer.phar fix
   ```

3. To fix a specific file or directory:
   ```
   php php-cs-fixer.phar fix path/to/file/or/directory
   ```

## Configuration

The PHP CS Fixer configuration is stored in `.php-cs-fixer.dist.php`. You can modify this file to change the coding standards rules applied to your project.

## Integrating with Your Workflow

- Consider running PHP CS Fixer before committing your code or as part of your CI/CD pipeline.
- You can add a pre-commit hook to automatically run PHP CS Fixer on changed files.

For more detailed information, visit the [PHP CS Fixer documentation](https://cs.symfony.com/).

================
File: README.md
================
# Topdata TopFeed for Shopware 6

## Description
Topdata TopFeed is a Shopware 6 plugin that enables automatic synchronization of product data between your Shopware store and TopData's cloud-based web service. This plugin enhances your product pages with rich, up-to-date information and improves cross-selling capabilities.

## Features
- Automatic product data synchronization (name, description, brand, EAN, MPN, images, specifications)
- Enhanced product relationships (variants, alternates, bundles, accessories)
- Compatible devices display
- Customizable product detail page with additional tabs and information
- Configurable cross-selling features
- Support for color and capacity variants

## Requirements
- **Shopware 6.7** (minimum requirement)
- PHP 8.1 or higher
- TopData Connector for Shopware 6 (version 8.0.0 or later)
- TopData Foundation for Shopware 6 (version 1.0 or later)

## Installation
1. Install the TopData Connector for Shopware 6 if you haven't already.
2. Upload the plugin files to your Shopware installation's `custom/plugins` directory.
3. Install and activate the plugin through the Shopware Administration panel.

## Configuration
After installation, you can configure the plugin in the Shopware Administration panel under Settings > System > Plugins > Topdata TopFeed.

Configuration options include:
- Import settings for various product data types
- Display settings for product detail pages
- Cross-selling options
- Product menu and list display options

## Usage
Once configured, the plugin will automatically sync product data from TopData's service. The enhanced product information and relationships will be visible on your store's product pages.

## Release Notes - Version 1.0.0 (SW 6.7 Migration)

### What's New
- **Full Shopware 6.7 Compatibility**: Complete migration to support Shopware 6.7
- **Enhanced Admin Interface**: Updated admin components to use new Shopware 6.7 UI framework
- **Improved Backend Services**: Refactored helper services for better performance and maintainability
- **Code Quality Improvements**: Added native type declarations and improved code structure

### Migration Changes
- **Admin Components**: Migrated from legacy template overrides to programmatic component extensions using `<mt-tabs>`
- **Backend Services**: Updated all services to use `TopfeedHelperServiceV2` for improved data handling
- **Storefront Templates**: Cleaned up deprecated modal popup code and streamlined template structure
- **Dependencies**: Updated to require Shopware 6.7.* and PHP 8.1+

### Breaking Changes
- **Minimum Requirements**: Now requires Shopware 6.7 or higher
- **PHP Version**: Minimum PHP version increased to 8.1
- **Connector Version**: Requires TopData Connector version 8.0.0 or later

### Upgrade Instructions
1. Ensure your Shopware installation is updated to version 6.7
2. Update PHP to version 8.1 or higher
3. Update TopData Connector to version 8.0.0 or later
4. Install the new TopData Foundation plugin (version 1.0 or later)
5. Upload and activate the updated TopFeed plugin
6. Clear cache and rebuild storefront assets

## Support
For support and more information, please visit: https://www.topdata.de/topfeed

## About TopData
TopData Software GmbH specializes in providing comprehensive product data solutions for e-commerce platforms. Visit [www.topdata.de](https://www.topdata.de) for more information about our services.

## License
This plugin is released under the MIT License. See the LICENSE file for more details.

================
File: rector.md
================
# Rector Usage Instructions

Rector is a tool for instant upgrades and automated refactoring of your PHP code. This document provides basic instructions on how to use Rector in this project.

## Running Rector

1. To perform a dry run (see what would be changed without actually changing files):
   ```
   vendor/bin/rector process src --dry-run
   ```

2. To apply changes:
   ```
   vendor/bin/rector process src
   ```

3. To process a specific file or directory:
   ```
   vendor/bin/rector process path/to/file/or/directory
   ```

## Configuration

The Rector configuration is stored in `rector.php`. You can modify this file to change the rules applied to your project.

## Integrating with Your Workflow

- Consider running Rector before committing your code or as part of your CI/CD pipeline.
- You can add a pre-commit hook to automatically run Rector on changed files.

For more detailed information, visit the [Rector documentation](https://getrector.org/).

================
File: rector.phar
================
Not Found

================
File: rector.php
================
<?php

/**
 * This file is a config file for rector
 *
 * This script configures the rules and sets for the rector process.
 */

declare(strict_types=1);

use Rector\CodeQuality\Rector\BooleanAnd\RemoveUselessIsObjectCheckRector;
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\Property\RemoveUselessVarTagRector;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector;

return static function (RectorConfig $rectorConfig): void {

    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_81, // shopware 6.1 is using php 8.1
//         SetList::TYPE_DECLARATION, // it breaks return with types for method overwrites in inherited classes
//            SetList::DEAD_CODE,
        SetList::CODE_QUALITY,
        SetList::TYPE_DECLARATION,
        SetList::EARLY_RETURN,
    ]);

    // ---- Apply specific rules for adding typed properties and property promotion
    $rectorConfig->rule(RemoveUselessVarTagRector::class);
    $rectorConfig->rule(RemoveUselessIsObjectCheckRector::class);
    $rectorConfig->rule(CompleteDynamicPropertiesRector::class);

    // ---- Add rule for converting annotations to attributes
    $rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [
        new AnnotationToAttribute('Symfony\Component\Routing\Annotation\Route'),
        new AnnotationToAttribute('Doctrine\ORM\Mapping\Entity'),
        new AnnotationToAttribute('Doctrine\ORM\Mapping\Column'),
        // Add more annotations as needed
    ]);


};

================
File: vite.config.js
================
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  build: {
    target: 'esnext',
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return id.toString().split('node_modules/')[1].split('/')[0].toString();
          }
        }
      }
    }
  }
});



================================================================
End of Codebase
================================================================
