feat: watchlist on TYPO3 13

Watchlist page (/merkliste/) and its list action, which renders the
hotel/product pairs a visitor stored in sessionStorage. The "remember"
buttons on teasers and detail pages had nowhere to show their list
since the static switch.

Compared with TYPO3 10 (page, empty and invalid input, unknown and
1 to 25 real pairs): identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-21 17:28:10 +02:00
1 parent 214b4b6b86
commit 00034c99ae
6 files changed
+233

No files matched your search

@@ -0,0 +1,105 @@
<?php
namespace EP\EpProducts\Controller;
/***************************************************************
*
* Copyright notice
*
* (c) 2018 Björn Fromme <[email protected]>, dreipunktnull
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Psr\Http\Message\ResponseInterface;
use EP\EpProducts\Domain\Repository\ProductRepository;
use EP\EpProducts\Service\FilterService;
use EP\EpProducts\Service\SearchResultService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class AjaxWatchlistController extends ActionController
{
/**
* @var ProductRepository
*/
protected $productRepository;
/**
* @var SearchResultService
*/
protected $searchResultService;
/**
* @var FilterService
*/
protected $filterService;
/**
* @param ProductRepository $productRepository
* @param SearchResultService $searchResultService
* @param FilterService $filterService
*/
public function __construct
(
ProductRepository $productRepository,
SearchResultService $searchResultService,
FilterService $filterService
)
{
$this->productRepository = $productRepository;
$this->searchResultService = $searchResultService;
$this->filterService = $filterService;
}
public function initializeAction(): void
{
if ($this->request->hasArgument('watchlistUids')) {
$watchlistUids = json_decode($this->request->getArgument('watchlistUids'), true, 512, JSON_THROW_ON_ERROR);
$this->request = $this->request->withArgument('watchlistUids', $watchlistUids);
}
}
/**
* @param array $watchlistUids
* @return string
* @throws \Doctrine\DBAL\DBALException
*/
public function listAction(array $watchlistUids = []): ResponseInterface
{
$productUids = array_map(function ($uid) {
[ , $productUid ] = GeneralUtility::trimExplode(':', $uid);
return $productUid;
}, $watchlistUids);
$hotelUids = array_map(function ($uid) {
[ $hotelUid, ] = GeneralUtility::trimExplode(':', $uid);
return $hotelUid;
}, $watchlistUids);
$watchlistData = $this->productRepository->getWatchlist($productUids, $hotelUids);
$watchlist = $this->searchResultService->preprocess($watchlistData);
$this->view->assignMultiple([
'watchlist' => $watchlist->getData(),
'total' => $watchlist->getTotal()
]);
return $this->htmlResponse();
}
}
@@ -0,0 +1,43 @@
<?php
namespace EP\EpProducts\Controller;
/***************************************************************
*
* Copyright notice
*
* (c) 2018 Björn Fromme <[email protected]>, dreipunktnull
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class WatchlistController extends ActionController
{
public function listAction(): ResponseInterface
{
$data = $this->request->getAttribute('currentContentObject')->data;
$this->view->assign('data', $data);
$this->view->assign('referringPageUid', $this->request->getAttribute('frontend.page.information')->getId());
return $this->htmlResponse();
}
}
@@ -13,6 +13,12 @@ call_user_func(function () {
'Suchergebnis'
);
ExtensionUtility::registerPlugin(
'EpProducts',
'watchlist',
'Merkliste'
);
ExtensionUtility::registerPlugin(
'EpProducts',
'product_detail',
+12
View File
@@ -2,9 +2,11 @@
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use EP\EpProducts\Controller\ProductController;
use EP\EpProducts\Controller\SearchController;
use EP\EpProducts\Controller\WatchlistController;
use EP\EpProducts\Controller\AjaxFilterPanelController;
use EP\EpProducts\Controller\AjaxSearchController;
use EP\EpProducts\Controller\AjaxTableController;
use EP\EpProducts\Controller\AjaxWatchlistController;
use EP\EpProducts\Controller\AjaxCalendarController;
use EP\EpProducts\Controller\AjaxTeaserPopupController;
use EP\EpProducts\Routing\Aspect\PassthroughMapper;
@@ -62,12 +64,21 @@ $boot = function () {
]
);
ExtensionUtility::configurePlugin(
'EpProducts',
'watchlist',
[
WatchlistController::class => 'list',
]
);
ExtensionUtility::configurePlugin(
'EpProducts',
'ajax',
[
AjaxSearchController::class => 'searchresult',
AjaxFilterPanelController::class => 'index',
AjaxWatchlistController::class => 'list',
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
AjaxCalendarController::class => 'priceCalendar,priceTable',
AjaxTeaserPopupController::class => 'concept',
@@ -75,6 +86,7 @@ $boot = function () {
[
AjaxSearchController::class => 'searchresult',
AjaxFilterPanelController::class => 'index',
AjaxWatchlistController::class => 'list',
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
AjaxTeaserPopupController::class => 'concept',
]
@@ -0,0 +1,44 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{watchlist -> f:count()} == 0">
<f:then>
<div class="alert alert-info">
Deine Merkliste ist aktuell leer...
</div>
</f:then>
<f:else>
<f:for each="{watchlist}" as="section">
<div id="concept-{section.concept.uid}">
<h4 class="headline headline--tertiary headline--underlined">
{section.concept.name}
</h4>
<div class="flex flex-col space-y-4 md:space-y-8 mb-8">
<f:for each="{section.items}" as="item">
<f:render partial="Product/Teaser" section="Teaser" arguments="{
concept: section.concept,
hotel: item.hotel,
product: item.product,
country: item.country,
region: item.region,
productImage: item.hotel.images.original.0,
nights: item.nights,
board: item.board,
minPrice: item.minPrice,
daytrip: product.daytrip,
season: product.season,
dateFrom: item.minDateStart,
dateTo: item.maxDateEnd,
forceHotelUid: 1,
landscape: 1,
referringPageUri: referringPageUri
}"/>
</f:for>
</div>
</div>
</f:for>
</f:else>
</f:if>
</html>
@@ -0,0 +1,23 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default"/>
<f:section name="Main">
<ep:container data="{data}">
<div class="relative"
data-controller="watchlist"
data-watchlist-uri-value="{ep:uri.ajax(action: 'list', controller: 'AjaxWatchlist', pageUid: '{settings.defaultAjaxUid}')}">
<div data-watchlist-target="container"></div>
<div class="absolute top-0 left-0 inset-0 bg-white/85 hidden"
data-watchlist-target="loadingIndicator">
<svg class="w-6 h-6 text-ep-primary-light absolute top-1/2 left-1/2 transform -translate-xy-1/2">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-spinner"></use>
</svg>
</div>
</div>
</ep:container>
</f:section>
</html>