├── .gitignore ├── registration.php ├── .styleci.yml ├── .gitlab-ci.yml ├── etc ├── module.xml ├── frontend │ └── routes.xml ├── di.xml └── widget.xml ├── .github └── workflows │ ├── phpcs.yml │ ├── phpcompatibility.yml │ ├── phpcsfixer.yml │ └── phpstan.yml ├── phpstan.neon ├── Block ├── Link.php ├── Navigation │ └── State.php ├── Navigation.php ├── Product │ └── ListProduct.php └── Widget │ └── Special.php ├── .travis.yml ├── view └── frontend │ ├── layout │ ├── default.xml │ └── special_index_index.xml │ └── templates │ └── product │ └── widget │ └── special │ ├── column │ ├── special_images_list.phtml │ ├── special_names_list.phtml │ └── special_default_list.phtml │ └── content │ ├── special_grid.phtml │ └── special_list.phtml ├── composer.json ├── Controller └── Index │ └── Index.php ├── .php-cs-fixer.dist.php ├── Model ├── Layer │ └── Resolver.php ├── ResourceModel │ └── Layer │ │ └── Filter │ │ └── Price.php └── Layer.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: M2 Coding Standard 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | static: 6 | name: M2 Coding Standard 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: extdn/github-actions-m2/magento-coding-standard/7.4@master -------------------------------------------------------------------------------- /.github/workflows/phpcompatibility.yml: -------------------------------------------------------------------------------- 1 | name: PHP Compatibility 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | static: 6 | name: PHP Compatibility 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: extdn/github-actions-m2/php-compatibility/7.4@master -------------------------------------------------------------------------------- /.github/workflows/phpcsfixer.yml: -------------------------------------------------------------------------------- 1 | name: php-cs-fixer 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@master 8 | - name: PHP CS Fixer 9 | run: docker run --rm -v $PWD:/code domw/php-cs-fixer php-cs-fixer fix --dry-run --diff --stop-on-violation --allow-risky=yes ./ -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 0 3 | autoload_directories: 4 | - Block 5 | - Controller 6 | - Model 7 | - view 8 | excludes_analyse: 9 | - 'vendor' 10 | ignoreErrors: 11 | - '#invalid typehint#' 12 | - '#unknown class#' 13 | reportUnmatchedIgnoredErrors: false -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: M2 PHPStan 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | phpstan: 6 | name: M2 PHPStan 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: extdn/github-actions-m2/magento-phpstan/7.4@master 11 | with: 12 | composer_name: dominicwatts/special -------------------------------------------------------------------------------- /etc/frontend/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Block/Link.php: -------------------------------------------------------------------------------- 1 | getTemplate()) { 17 | return parent::_toHtml(); 18 | } 19 | return '
  • getLinkAttributes() . ' >' . $this->escapeHtml($this->getLabel()) . '
  • '; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | services: 4 | - docker 5 | matrix: 6 | fast_finish: true 7 | include: 8 | - php: '5.6' 9 | - php: '7.0' 10 | - php: '7.1' 11 | env: SNIFF=1 12 | - php: '7.2' 13 | - php: nightly 14 | allow_failures: 15 | - php: '5.6' 16 | - php: nightly 17 | before_install: 18 | - if [[ "$SNIFF" == "1" ]]; then docker pull domw/phpcs; fi 19 | - phpenv rehash 20 | script: 21 | - find -L . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l 22 | - if [[ "$SNIFF" == "1" ]]; then docker run --rm -v $PWD:/code:ro domw/phpcs phpcs --colors --standard=Magento2 --report=full,summary,gitblame --extensions=php,phtml ./; fi 23 | -------------------------------------------------------------------------------- /view/frontend/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Special Offers 8 | special-offers 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Block/Navigation/State.php: -------------------------------------------------------------------------------- 1 | _catalogLayer = $layerResolver->get(); 29 | parent::__construct($context, $layerResolver, $data); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dominicwatts/special", 3 | "description": "Special offers landing page with layered navigation and widget", 4 | "type": "magento2-module", 5 | "license": "proprietary", 6 | "version": "1.2.1", 7 | "authors": [ 8 | { 9 | "name": "Dominic Xigen", 10 | "email": "dominic@xigen.co.uk" 11 | } 12 | ], 13 | "minimum-stability": "dev", 14 | "repositories": [ 15 | { 16 | "type": "composer", 17 | "url": "https://repo.magento.com/" 18 | } 19 | ], 20 | "require": { 21 | "magento/framework": "*", 22 | "php": ">=7.1.0", 23 | "magento/module-layered-navigation": "*", 24 | "magento/module-catalog": "*" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Xigen\\Special\\": "" 29 | }, 30 | "files": [ 31 | "registration.php" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Block/Navigation.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Controller/Index/Index.php: -------------------------------------------------------------------------------- 1 | pageFactory = $pageFactory; 25 | parent::__construct($context); 26 | } 27 | 28 | /** 29 | * Execute view action 30 | * @return \Magento\Framework\Controller\ResultInterface 31 | */ 32 | public function execute() 33 | { 34 | $resultPage = $this->pageFactory->create(); 35 | $resultPage->getConfig() 36 | ->getTitle() 37 | ->set(__('Special Offers')); 38 | return $resultPage; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in('Block') 13 | ->in('Controller') 14 | ->in('Model') 15 | ->in('view') 16 | ->name('*.phtml'); 17 | 18 | $config = new PhpCsFixer\Config(); 19 | $config->setFinder($finder) 20 | ->setRules([ 21 | '@PSR2' => true, 22 | 'array_syntax' => ['syntax' => 'short'], 23 | 'concat_space' => ['spacing' => 'one'], 24 | 'include' => true, 25 | 'new_with_braces' => true, 26 | 'no_empty_statement' => true, 27 | 'no_extra_blank_lines' => true, 28 | 'no_leading_import_slash' => true, 29 | 'no_leading_namespace_whitespace' => true, 30 | 'no_multiline_whitespace_around_double_arrow' => true, 31 | 'multiline_whitespace_before_semicolons' => true, 32 | 'no_singleline_whitespace_before_semicolons' => true, 33 | 'no_trailing_comma_in_singleline_array' => true, 34 | 'no_unused_imports' => true, 35 | 'no_whitespace_in_blank_line' => true, 36 | 'object_operator_without_whitespace' => true, 37 | 'ordered_imports' => true, 38 | 'standardize_not_equals' => true, 39 | 'ternary_operator_spaces' => true, 40 | ]); 41 | return $config; 42 | -------------------------------------------------------------------------------- /Model/Layer/Resolver.php: -------------------------------------------------------------------------------- 1 | layer = $layer; 34 | $this->layersPool = $layersPool; 35 | parent::__construct($objectManager, $layersPool); 36 | } 37 | 38 | public function create($layerType) 39 | { 40 | if (isset($this->layer)) { 41 | throw new \RuntimeException('Catalog Layer has been already created'); 42 | } 43 | if (!isset($this->layersPool[$layerType])) { 44 | throw new \InvalidArgumentException($layerType . ' does not belong to any registered layer'); 45 | } 46 | $this->layer = $this->objectManager->create($this->layersPool[$layerType]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Special Offers Page and Widget # 2 | 3 | [![M2 Coding Standard](https://github.com/DominicWatts/Special/actions/workflows/phpcs.yml/badge.svg)](https://github.com/DominicWatts/Special/actions/workflows/phpcs.yml) 4 | 5 | [![M2 PHPStan](https://github.com/DominicWatts/Special/actions/workflows/phpstan.yml/badge.svg)](https://github.com/DominicWatts/Special/actions/workflows/phpstan.yml) 6 | 7 | [![PHP Compatibility](https://github.com/DominicWatts/Special/actions/workflows/phpcompatibility.yml/badge.svg)](https://github.com/DominicWatts/Special/actions/workflows/phpcompatibility.yml) 8 | 9 | [![php-cs-fixer](https://github.com/DominicWatts/Special/actions/workflows/phpcsfixer.yml/badge.svg)](https://github.com/DominicWatts/Special/actions/workflows/phpcsfixer.yml) 10 | 11 | Frontend controller to display products with special offers with layered navigation. 12 | 13 | For products to display they must be on special offer and with the special from and to date range. Plus any additional rules that would apply to a product collection on your store such as visibility or stock. 14 | 15 | ![Screenshot](https://i.snag.gy/3GZ6wr.jpg) 16 | 17 | The widget needs to be configured from the admin 18 | 19 | # Install instructions # 20 | 21 | `composer require dominicwatts/special` 22 | 23 | `php bin/magento setup:upgrade` 24 | 25 | `php bin/magento setup:di:compile` 26 | 27 | # Usage instructions # 28 | 29 | Apply special offers to products 30 | 31 | Go to `/special-offers` or use header link 32 | 33 | 34 | -------------------------------------------------------------------------------- /view/frontend/templates/product/widget/special/column/special_images_list.phtml: -------------------------------------------------------------------------------- 1 | 8 | getProductCollection()) && $_products->getSize()): ?> 9 |
    10 |
    11 | escapeHtml(__('Special Offers')) ?> 12 |
    13 |
    14 | getNameInLayout(); ?> 15 |
      17 | getItems() as $_product): ?> 18 |
    1. 19 | 21 | 22 | getImage($_product, 'new_products_images_only_widget')->toHtml() ?> 23 | 24 |
    2. 25 | 26 |
    27 | getPagerHtml() ?> 28 |
    29 |
    30 | 31 | -------------------------------------------------------------------------------- /view/frontend/templates/product/widget/special/column/special_names_list.phtml: -------------------------------------------------------------------------------- 1 | 9 | getProductCollection()) && $_products->getSize()): ?> 10 |
    11 |
    12 | escapeHtml(__('Special Offers')) ?> 13 |
    14 |
    15 | getNameInLayout(); ?> 16 |
      18 | getItems() as $_product): ?> 19 |
    1. 20 | 21 | 24 | helper( 25 | Magento\Catalog\Helper\Output::class 26 | )->productAttribute($_product, $_product->getName(), 'name') ?> 27 | 28 | 29 |
    2. 30 | 31 |
    32 | getPagerHtml() ?> 33 |
    34 |
    35 | 36 | -------------------------------------------------------------------------------- /Block/Product/ListProduct.php: -------------------------------------------------------------------------------- 1 | _catalogLayer = $layerResolver->get(); 50 | $this->_postDataHelper = $postDataHelper; 51 | $this->categoryRepository = $categoryRepository; 52 | $this->urlHelper = $urlHelper; 53 | parent::__construct( 54 | $context, 55 | $postDataHelper, 56 | $layerResolver, 57 | $categoryRepository, 58 | $urlHelper, 59 | $data 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Model/ResourceModel/Layer/Filter/Price.php: -------------------------------------------------------------------------------- 1 | layer = $layerResolver->get(); 76 | $this->session = $session; 77 | $this->storeManager = $storeManager; 78 | $this->_eventManager = $eventManager; 79 | $this->priceTableResolver = $priceTableResolver 80 | ?? ObjectManager::getInstance()->get(IndexScopeResolverInterface::class); 81 | $this->httpContext = $httpContext ?? ObjectManager::getInstance()->get(Context::class); 82 | $this->dimensionFactory = $dimensionFactory ?? ObjectManager::getInstance()->get(DimensionFactory::class); 83 | parent::__construct( 84 | $context, 85 | $eventManager, 86 | $layerResolver, 87 | $session, 88 | $storeManager, 89 | $connectionName, 90 | $priceTableResolver, 91 | $httpContext, 92 | $dimensionFactory 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /etc/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | List of products that are set as special Products 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 5 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 23 | 24 | 27 | 30 | 34 | 38 | 42 | 43 | 44 | 45 | 46 | 86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache. 47 | 48 | 49 | 50 | 51 |