feat: product search on TYPO3 13
Search page (searchresult plugin), AJAX results and filter panel, the
mobile filter overlay, search links ("Alle anzeigen", menu icon) and
the /reiseauswahl/{name} landing redirects are back. The search bar in
the page header stays out (SearchbarController not ported); on detail
pages the menu icon links to the search page instead of toggling it.
- Extbase requests are immutable: withArgument() instead of
setArgument()
- StaticSearchParamsMiddleware runs after site resolution and before
page resolution (the old position is a cycle in v13); FilterService
copes with the missing TypoScript there, as TYPO3 10 did
- Search and filter forms are sent URL-encoded instead of multipart, so
production can keep rejecting multipart requests
- ski-wochenenden: websiteTitle from the old sys_template sitetitle
Compared with TYPO3 10 on the same data (249 cases: search pages with
all 41 preset filters, landing redirects, AJAX results and filter
panel, GET and POST): 186 identical, 63 with the same results in a
different order (PHP 8 sorts stably, ties came out in arbitrary order
on PHP 7.4). Processed image names differ by their configuration hash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
18757781bb
commit
72465053cd
36 files changed
+1643
-79
No files matched your search
@@ -57,3 +57,4 @@ routes:
|
||||
route: robots.txt
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
websiteTitle: 'Ski-Wochenenden mit E&P Reisen und Events'
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Controller;
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
* Copyright notice
|
||||
*
|
||||
* (c) 2021 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\Service\FilterService;
|
||||
use EP\EpProducts\Service\SearchResultService;
|
||||
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
class AjaxFilterPanelController extends ActionController
|
||||
{
|
||||
|
||||
use RequestArgumentTypeConversionTrait;
|
||||
|
||||
/**
|
||||
* @var FilterService
|
||||
*/
|
||||
protected $filterService;
|
||||
|
||||
/**
|
||||
* @var SearchResultService
|
||||
*/
|
||||
private $searchResultService;
|
||||
|
||||
/**
|
||||
* @param FilterService $filterService
|
||||
*/
|
||||
public function __construct(FilterService $filterService, SearchResultService $searchResultService)
|
||||
{
|
||||
$this->filterService = $filterService;
|
||||
$this->searchResultService = $searchResultService;
|
||||
}
|
||||
|
||||
public function initializeAction(): void
|
||||
{
|
||||
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
|
||||
$this->processFilterSettingsArgument($forcedConceptUids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filterSettings
|
||||
*/
|
||||
public function indexAction(array $filterSettings): ResponseInterface
|
||||
{
|
||||
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'] ?? '');
|
||||
|
||||
$searchResult = $this
|
||||
->searchResultService
|
||||
->getResult($filterSettings, $excludedConceptUids);
|
||||
|
||||
$filterOptions = $this->filterService->collectFilterOptionsFromSearchResult($searchResult);
|
||||
|
||||
$resultsUri = $this->uriBuilder
|
||||
->reset()
|
||||
->setCreateAbsoluteUri(true)
|
||||
->setTargetPageUid((int) ($this->settings['defaultSearchPageUid'] ?? null))
|
||||
->uriFor(
|
||||
'searchresult',
|
||||
['filterSettings' => $filterSettings],
|
||||
'Search',
|
||||
'epproducts',
|
||||
'searchresult'
|
||||
);
|
||||
|
||||
$filterOptions->setResultsUri($resultsUri);
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'filterSettings' => $filterSettings,
|
||||
'filterOptions' => $filterOptions,
|
||||
'stimulusController' => 'mobile-search',
|
||||
]);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Controller;
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
* Copyright notice
|
||||
*
|
||||
* (c) 2016 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\Service\FilterService;
|
||||
use EP\EpProducts\Service\SearchResultService;
|
||||
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
|
||||
use EP\EpProducts\Utility\DateUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
class AjaxSearchController extends ActionController
|
||||
{
|
||||
|
||||
use RequestArgumentTypeConversionTrait;
|
||||
|
||||
/**
|
||||
* @var FilterService
|
||||
*/
|
||||
protected $filterService;
|
||||
|
||||
/**
|
||||
* @var SearchResultService
|
||||
*/
|
||||
protected $searchResultService;
|
||||
|
||||
/**
|
||||
* @param FilterService $filterService
|
||||
* @param SearchResultService $searchResultService
|
||||
*/
|
||||
public function __construct(FilterService $filterService, SearchResultService $searchResultService)
|
||||
{
|
||||
$this->filterService = $filterService;
|
||||
$this->searchResultService = $searchResultService;
|
||||
}
|
||||
|
||||
public function initializeSearchresultAction()
|
||||
{
|
||||
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
|
||||
$this->processFilterSettingsArgument($forcedConceptUids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filterSettings
|
||||
*/
|
||||
public function searchresultAction(array $filterSettings): ResponseInterface
|
||||
{
|
||||
$dateSelected = false;
|
||||
$organic = true;
|
||||
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
|
||||
$excludedConceptUids = GeneralUtility::intExplode(',', $this->settings['excludedConceptUids'] ?? '', true);
|
||||
|
||||
if (isset($filterSettings['dateFrom']) || isset($filterSettings['dateTo'])) {
|
||||
$dateSelected = true;
|
||||
}
|
||||
|
||||
$searchResult = $this
|
||||
->searchResultService
|
||||
->getResult($filterSettings, $excludedConceptUids);
|
||||
|
||||
// No results? First retry without date constraint if any
|
||||
if ((!empty($filterSettings['dateFrom']) || !empty($filterSettings['dateTo'])) && 0 === $searchResult->getTotal()) {
|
||||
$filterSettings['dateFrom'] = null;
|
||||
$filterSettings['dateTo'] = null;
|
||||
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
|
||||
$organic = false;
|
||||
}
|
||||
|
||||
// Still no results? Remove all constraints and search again with defaults
|
||||
if (0 === $searchResult->getTotal()) {
|
||||
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
|
||||
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
|
||||
$organic = false;
|
||||
}
|
||||
|
||||
$filterOptions = $this->filterService->collectFilterOptionsFromSearchResult($searchResult);
|
||||
|
||||
$referringPageUri = $this
|
||||
->uriBuilder
|
||||
->reset()
|
||||
->setTargetPageUid((int)($this->settings['defaultSearchPageUid'] ?? null))
|
||||
->setCreateAbsoluteUri(true)
|
||||
->uriFor(
|
||||
'searchresult',
|
||||
['filterSettings' => $filterSettings],
|
||||
'Search',
|
||||
'epproducts',
|
||||
'searchresult'
|
||||
)
|
||||
;
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'filterSettings' => $filterSettings,
|
||||
'filterOptions' => $filterOptions,
|
||||
'searchResult' => $searchResult->getData(),
|
||||
'referringPageUri' => $referringPageUri,
|
||||
'stimulusController' => 'search',
|
||||
'meta' => [
|
||||
'organic' => $organic,
|
||||
'total' => $searchResult->getTotal(),
|
||||
'dateRange' => DateUtility::formatDateRange($filterSettings['dateFrom'], $filterSettings['dateTo']),
|
||||
'dateSelected' => $dateSelected,
|
||||
],
|
||||
]);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -106,11 +106,11 @@ class AjaxTableController extends ActionController
|
||||
{
|
||||
if ($this->request->hasArgument('dateFrom')) {
|
||||
$dateFrom = $this->request->getArgument('dateFrom');
|
||||
$this->request->setArgument('dateFrom', $dateFrom === '' ? null : $dateFrom);
|
||||
$this->request = $this->request->withArgument('dateFrom', $dateFrom === '' ? null : $dateFrom);
|
||||
}
|
||||
if ($this->request->hasArgument('dateTo')) {
|
||||
$dateTo = $this->request->getArgument('dateTo');
|
||||
$this->request->setArgument('dateTo', $dateTo === '' ? null : $dateTo);
|
||||
$this->request = $this->request->withArgument('dateTo', $dateTo === '' ? null : $dateTo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Controller;
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
* Copyright notice
|
||||
*
|
||||
* (c) 2016 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\Service\FilterService;
|
||||
use EP\EpProducts\Service\FilterSettingsEncoder;
|
||||
use EP\EpProducts\Service\SearchResultService;
|
||||
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
|
||||
use EP\EpProducts\Utility\DateUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
class SearchController extends ActionController
|
||||
{
|
||||
|
||||
use RequestArgumentTypeConversionTrait;
|
||||
|
||||
/**
|
||||
* @var FilterService
|
||||
*/
|
||||
protected $filterService;
|
||||
|
||||
/**
|
||||
* @var SearchResultService
|
||||
*/
|
||||
private $searchResultService;
|
||||
|
||||
/**
|
||||
* @param FilterService $filterService
|
||||
*/
|
||||
public function __construct(FilterService $filterService, SearchResultService $searchResultService)
|
||||
{
|
||||
$this->filterService = $filterService;
|
||||
$this->searchResultService = $searchResultService;
|
||||
}
|
||||
|
||||
public function initializeProcessSearchAction()
|
||||
{
|
||||
$this->processSearchParamsArgument();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $searchParams
|
||||
*/
|
||||
public function processSearchAction(array $searchParams): ResponseInterface
|
||||
{
|
||||
$filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams);
|
||||
|
||||
$redirectUri = $this->uriBuilder
|
||||
->reset()
|
||||
->setCreateAbsoluteUri(true)
|
||||
->uriFor(
|
||||
'searchresult',
|
||||
['filterSettings' => $filterSettings],
|
||||
null,
|
||||
null,
|
||||
'searchresult'
|
||||
);
|
||||
|
||||
return $this->redirectToUri($redirectUri);
|
||||
}
|
||||
|
||||
public function initializeSearchresultAction()
|
||||
{
|
||||
// Process encoded filtersettings from static search result.
|
||||
// Explicit filter settings win: they carry the visitor's current selection,
|
||||
// whereas a stale 'f' riding along on a link would silently replace it.
|
||||
$encodedFilterSettings = $this->request->getQueryParams()['f'] ?? null;
|
||||
if ($encodedFilterSettings && !$this->request->hasArgument('filterSettings')) {
|
||||
$filterSettingsEncoder = new FilterSettingsEncoder();
|
||||
$filterSettings = $filterSettingsEncoder->decode($encodedFilterSettings);
|
||||
if (count($filterSettings) > 0) {
|
||||
$this->request = $this->request->withArgument('filterSettings', $filterSettings);
|
||||
}
|
||||
}
|
||||
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
|
||||
$this->processFilterSettingsArgument($forcedConceptUids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filterSettings
|
||||
*/
|
||||
public function searchresultAction(array $filterSettings): ResponseInterface
|
||||
{
|
||||
$dateSelected = false;
|
||||
$organic = true;
|
||||
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true);
|
||||
$excludedConceptUids = GeneralUtility::intExplode(',', $this->settings['excludedConceptUids'] ?? '');
|
||||
|
||||
if (!empty($filterSettings['dateFrom']) || !empty($filterSettings['dateTo'])) {
|
||||
$dateSelected = true;
|
||||
}
|
||||
|
||||
$searchResult = $this
|
||||
->searchResultService
|
||||
->getResult($filterSettings, $excludedConceptUids);
|
||||
|
||||
// No results? First retry without date constraint if any
|
||||
if ((!empty($filterSettings['dateFrom']) || !empty($filterSettings['dateTo'])) && 0 === $searchResult->getTotal()) {
|
||||
$filterSettings['dateFrom'] = null;
|
||||
$filterSettings['dateTo'] = null;
|
||||
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
|
||||
$organic = false;
|
||||
}
|
||||
|
||||
// Still no results? Remove all constraints and search again with defaults
|
||||
if (0 === $searchResult->getTotal()) {
|
||||
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
|
||||
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
|
||||
$organic = false;
|
||||
}
|
||||
|
||||
$filterOptions = $this->filterService->collectFilterOptionsFromSearchResult($searchResult);
|
||||
|
||||
$referringPageUri = $this
|
||||
->uriBuilder
|
||||
->reset()
|
||||
->setTargetPageUid($this->settings['defaultSearchPageUid'])
|
||||
->setCreateAbsoluteUri(true)
|
||||
->uriFor(
|
||||
'searchresult',
|
||||
['filterSettings' => $filterSettings],
|
||||
'Search',
|
||||
'epproducts',
|
||||
'searchresult'
|
||||
)
|
||||
;
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'filterSettings' => $filterSettings,
|
||||
'filterOptions' => $filterOptions,
|
||||
'searchResult' => $searchResult->getData(),
|
||||
'referringPageUri' => $referringPageUri,
|
||||
'stimulusController' => 'search',
|
||||
'meta' => [
|
||||
'organic' => $organic,
|
||||
'total' => $searchResult->getTotal(),
|
||||
'dateRange' => DateUtility::formatDateRange($filterSettings['dateFrom'], $filterSettings['dateTo']),
|
||||
'dateSelected' => $dateSelected,
|
||||
],
|
||||
]);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
@@ -325,7 +325,7 @@ class FilterOptions implements \JsonSerializable
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return [
|
||||
'priceRanges' => $this->getPriceRanges(),
|
||||
|
||||
@@ -153,7 +153,8 @@ abstract class AbstractRepository extends Repository
|
||||
if (isset($filterSettings['priceRanges']) && is_array($filterSettings['priceRanges']) && count($filterSettings['priceRanges']) > 0) {
|
||||
$orX = $qb->expr()->or();
|
||||
foreach ($filterSettings['priceRanges'] as $priceRange) {
|
||||
[ $priceMin, $priceMax ] = FilterOptions::$priceRangeOptions[$priceRange];
|
||||
// An unknown range matches nothing, as on TYPO3 10
|
||||
[ $priceMin, $priceMax ] = FilterOptions::$priceRangeOptions[$priceRange] ?? [ null, null ];
|
||||
$orX = $orX->with($qb->expr()->and($qb->expr()->gte(
|
||||
'date.min_price',
|
||||
$qb->createNamedParameter($priceMin, Connection::PARAM_INT)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Middleware;
|
||||
|
||||
use EP\EpProducts\Service\FilterService;
|
||||
use EP\EpProducts\Service\FilterSettingsEncoder;
|
||||
use EP\EpProducts\Traits\DbConnectionTrait;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
class StaticSearchParamsMiddleware implements MiddlewareInterface
|
||||
{
|
||||
use DbConnectionTrait;
|
||||
|
||||
public const PREFIX_PATH_SEGMENT = 'reiseauswahl';
|
||||
public const REDIRECT_RESPONSE_CODE = 302;
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
/** @var NormalizedParams $params */
|
||||
$params = $request->getAttribute('normalizedParams');
|
||||
$uri = $params->getRequestUri();
|
||||
|
||||
if (strpos($uri, self::PREFIX_PATH_SEGMENT) === false) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
// Ensure trailing slash for comparison
|
||||
$uri = rtrim($uri, '/') . '/';
|
||||
|
||||
$qb = $this->getDbConnection()->createQueryBuilder();
|
||||
|
||||
$query = $qb
|
||||
->select('*')
|
||||
->from('tx_epproducts_domain_model_staticsearchparams')
|
||||
->where('CONCAT("/", :prefix, "/", path_segment, "/")=LOWER(:uri)')
|
||||
->andWhere('deleted = 0')
|
||||
->andWhere('hidden = 0')
|
||||
->setParameters([
|
||||
'prefix' => self::PREFIX_PATH_SEGMENT,
|
||||
'uri' => $uri,
|
||||
])
|
||||
;
|
||||
|
||||
$row = $query->executeQuery()->fetchAssociative();
|
||||
|
||||
if (!$row) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$filterService = GeneralUtility::makeInstance(FilterService::class);
|
||||
|
||||
$filterSettings = $filterService->getFilterSettingsFromStaticSearchParams($row);
|
||||
$filterSettingsEncoder = new FilterSettingsEncoder();
|
||||
$encodedFilterSettings = $filterSettingsEncoder->encode($filterSettings);
|
||||
|
||||
$searchPageUid = GeneralUtility::makeInstance(ExtensionConfiguration::class)
|
||||
->get('ep_products', 'searchPageUid');
|
||||
|
||||
$site = $request->getAttribute('site');
|
||||
$redirectUri = $site->getRouter()->generateUri($searchPageUid, ['f' => $encodedFilterSettings]);
|
||||
|
||||
return new RedirectResponse($redirectUri, self::REDIRECT_RESPONSE_CODE);
|
||||
}
|
||||
}
|
||||
@@ -333,8 +333,14 @@ class FilterService implements SingletonInterface
|
||||
{
|
||||
/** @var ConfigurationManager $configurationManager */
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
$settings = $configurationManager
|
||||
->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
|
||||
try {
|
||||
$settings = $configurationManager
|
||||
->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
|
||||
} catch (\RuntimeException) {
|
||||
// No TypoScript yet (StaticSearchParamsMiddleware runs before it): as on TYPO3 10, where
|
||||
// the setup was simply empty at that point, the filter goes without productsPid
|
||||
return null;
|
||||
}
|
||||
|
||||
return $settings ? $settings['plugin.']['tx_epproducts.']['settings.'] : null;
|
||||
}
|
||||
|
||||
@@ -103,29 +103,29 @@ class SearchResultService implements SingletonInterface
|
||||
foreach ($searchResultData as $row) {
|
||||
// Concepts
|
||||
if (!array_key_exists($row['conceptUid'], $data)) {
|
||||
$data[$row['conceptUid']] = [
|
||||
$data[($row['conceptUid'] ?? null)] = [
|
||||
'concept' => [
|
||||
'uid' => $row['conceptUid'],
|
||||
'name' => $row['conceptName'],
|
||||
'code' => $row['conceptCode'],
|
||||
'sorting' => $row['conceptSorting'],
|
||||
'popupText' => $row['conceptPopupText'],
|
||||
'uid' => ($row['conceptUid'] ?? null),
|
||||
'name' => ($row['conceptName'] ?? null),
|
||||
'code' => ($row['conceptCode'] ?? null),
|
||||
'sorting' => ($row['conceptSorting'] ?? null),
|
||||
'popupText' => ($row['conceptPopupText'] ?? null),
|
||||
],
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$items =& $data[$row['conceptUid']]['items'];
|
||||
$items =& $data[($row['conceptUid'] ?? null)]['items'];
|
||||
|
||||
$isAggregatedEvent = $aggregateEvents && 'evt' === $row['conceptCode'];
|
||||
$isAggregatedEvent = $aggregateEvents && 'evt' === ($row['conceptCode'] ?? null);
|
||||
|
||||
// Events are product-level search results. Other results (including
|
||||
// watchlist items) remain tied to a particular hotel.
|
||||
$key = $isAggregatedEvent
|
||||
? (string) $row['productUid']
|
||||
: $row['hotelUid'] . ':' . $row['productUid'];
|
||||
? (string) ($row['productUid'] ?? null)
|
||||
: ($row['hotelUid'] ?? null) . ':' . ($row['productUid'] ?? null);
|
||||
|
||||
$hotelCategory = Hotel::$categoryLabels[$row['hotelCategory']] ?? null;
|
||||
$hotelCategory = Hotel::$categoryLabels[($row['hotelCategory'] ?? null)] ?? null;
|
||||
|
||||
if (!array_key_exists($key, $items)) {
|
||||
$items[$key] = [
|
||||
@@ -134,53 +134,53 @@ class SearchResultService implements SingletonInterface
|
||||
'maxDateEnd' => null,
|
||||
'minPrice' => null,
|
||||
'detailPageUri' => null,
|
||||
'earlybird' => $row['earlybird'],
|
||||
'daytrip' => $row['daytrip'],
|
||||
'season' => $row['season'],
|
||||
'hideBookingButton' => $row['hideBookingButton'],
|
||||
'busAvailable' => $row['busAvailable'],
|
||||
'available' => $row['available'],
|
||||
'lowContingent' => $row['lowContingent'],
|
||||
'servicesIncluded' => unserialize($row['servicesIncluded'], ['allowed_classes' => false]),
|
||||
'earlybird' => ($row['earlybird'] ?? null),
|
||||
'daytrip' => ($row['daytrip'] ?? null),
|
||||
'season' => ($row['season'] ?? null),
|
||||
'hideBookingButton' => ($row['hideBookingButton'] ?? null),
|
||||
'busAvailable' => ($row['busAvailable'] ?? null),
|
||||
'available' => ($row['available'] ?? null),
|
||||
'lowContingent' => ($row['lowContingent'] ?? null),
|
||||
'servicesIncluded' => unserialize((string)($row['servicesIncluded'] ?? ''), ['allowed_classes' => false]),
|
||||
'hotel' => [
|
||||
'uid' => $row['hotelUid'],
|
||||
'name' => $row['hotelName'],
|
||||
'teaser' => $row['hotelTeaser'],
|
||||
'uid' => ($row['hotelUid'] ?? null),
|
||||
'name' => ($row['hotelName'] ?? null),
|
||||
'teaser' => ($row['hotelTeaser'] ?? null),
|
||||
'images' => $this->hotelImageService->getImages($row['hotelUid']),
|
||||
'fact' => $row['hotelFact'],
|
||||
'fact' => ($row['hotelFact'] ?? null),
|
||||
'category' => $hotelCategory,
|
||||
'type' => $row['hotelType'],
|
||||
'type' => ($row['hotelType'] ?? null),
|
||||
],
|
||||
'country' => [
|
||||
'uid' => $row['countryUid'],
|
||||
'code' => $row['countryCode'],
|
||||
'name' => $row['countryName'],
|
||||
'uid' => ($row['countryUid'] ?? null),
|
||||
'code' => ($row['countryCode'] ?? null),
|
||||
'name' => ($row['countryName'] ?? null),
|
||||
],
|
||||
'region' => [
|
||||
'uid' => $row['regionUid'],
|
||||
'name' => $row['regionName'],
|
||||
'fact' => $row['regionFact'],
|
||||
'feature' => $row['regionFeature'],
|
||||
'uid' => ($row['regionUid'] ?? null),
|
||||
'name' => ($row['regionName'] ?? null),
|
||||
'fact' => ($row['regionFact'] ?? null),
|
||||
'feature' => ($row['regionFeature'] ?? null),
|
||||
],
|
||||
'product' => [
|
||||
'uid' => $row['productUid'],
|
||||
'detailPage' => $row['productDetailPage'],
|
||||
'name' => $row['productName'],
|
||||
'subline' => $row['productSubline'],
|
||||
'feature' => $row['productFeature'],
|
||||
'fact' => $row['productFact'],
|
||||
'uid' => ($row['productUid'] ?? null),
|
||||
'detailPage' => ($row['productDetailPage'] ?? null),
|
||||
'name' => ($row['productName'] ?? null),
|
||||
'subline' => ($row['productSubline'] ?? null),
|
||||
'feature' => ($row['productFeature'] ?? null),
|
||||
'fact' => ($row['productFact'] ?? null),
|
||||
'images' => $isAggregatedEvent
|
||||
? $this->productImageService->getImages($row['productUid'])
|
||||
: [],
|
||||
],
|
||||
'board' => $row['board'],
|
||||
'boardUid' => $row['boardUid'],
|
||||
'board' => ($row['board'] ?? null),
|
||||
'boardUid' => ($row['boardUid'] ?? null),
|
||||
'dates' => [],
|
||||
'nights' => [],
|
||||
'teaserNights' => [],
|
||||
'rooms' => [],
|
||||
'new' => $row['new'],
|
||||
'skiPassIncluded' => $row['skiPassIncluded'],
|
||||
'new' => ($row['new'] ?? null),
|
||||
'skiPassIncluded' => ($row['skiPassIncluded'] ?? null),
|
||||
'filterMetadata' => [
|
||||
'destinations' => [],
|
||||
'hotelTypes' => [],
|
||||
@@ -194,15 +194,15 @@ class SearchResultService implements SingletonInterface
|
||||
|
||||
$destination = [
|
||||
'country' => [
|
||||
'uid' => $row['countryUid'],
|
||||
'code' => $row['countryCode'],
|
||||
'name' => $row['countryName'],
|
||||
'uid' => ($row['countryUid'] ?? null),
|
||||
'code' => ($row['countryCode'] ?? null),
|
||||
'name' => ($row['countryName'] ?? null),
|
||||
],
|
||||
'region' => [
|
||||
'uid' => $row['regionUid'],
|
||||
'name' => $row['regionName'],
|
||||
'fact' => $row['regionFact'],
|
||||
'feature' => $row['regionFeature'],
|
||||
'uid' => ($row['regionUid'] ?? null),
|
||||
'name' => ($row['regionName'] ?? null),
|
||||
'fact' => ($row['regionFact'] ?? null),
|
||||
'feature' => ($row['regionFeature'] ?? null),
|
||||
],
|
||||
];
|
||||
if (!\in_array($destination, $item['filterMetadata']['destinations'], false)) {
|
||||
@@ -212,7 +212,7 @@ class SearchResultService implements SingletonInterface
|
||||
if (!\in_array($hotelType, $item['filterMetadata']['hotelTypes'], false)) {
|
||||
$item['filterMetadata']['hotelTypes'][] = $hotelType;
|
||||
}
|
||||
$boardType = ['uid' => $row['boardUid'] ?? null, 'label' => $row['board']];
|
||||
$boardType = ['uid' => $row['boardUid'] ?? null, 'label' => ($row['board'] ?? null)];
|
||||
if (!\in_array($boardType, $item['filterMetadata']['boardTypes'], false)) {
|
||||
$item['filterMetadata']['boardTypes'][] = $boardType;
|
||||
}
|
||||
@@ -224,15 +224,15 @@ class SearchResultService implements SingletonInterface
|
||||
}
|
||||
|
||||
if (!\in_array($row['nights'], $item['nights'], false)) {
|
||||
$item['nights'][] = $row['nights'];
|
||||
$item['nights'][] = ($row['nights'] ?? null);
|
||||
}
|
||||
|
||||
if (!\in_array($row['dateStart'], $item['dates'], false)) {
|
||||
$item['dates'][] = $row['dateStart'];
|
||||
$item['dates'][] = ($row['dateStart'] ?? null);
|
||||
}
|
||||
|
||||
$dateStart = $row['dateStart'];
|
||||
$dateEnd = $row['dateEnd'];
|
||||
$dateStart = ($row['dateStart'] ?? null);
|
||||
$dateEnd = ($row['dateEnd'] ?? null);
|
||||
|
||||
if ($item['minDateStart'] === null
|
||||
|| $item['minDateStart'] > $dateStart) {
|
||||
@@ -245,26 +245,26 @@ class SearchResultService implements SingletonInterface
|
||||
}
|
||||
|
||||
if ($item['minPrice'] === null
|
||||
|| $item['minPrice'] > $row['minPrice']) {
|
||||
$item['minPrice'] = $row['minPrice'];
|
||||
|| $item['minPrice'] > ($row['minPrice'] ?? null)) {
|
||||
$item['minPrice'] = ($row['minPrice'] ?? null);
|
||||
// Teaser-specific offer data must describe the cheapest row.
|
||||
$item['teaserNights'] = [$row['nights']];
|
||||
$item['board'] = $row['board'];
|
||||
$item['boardUid'] = $row['boardUid'];
|
||||
$item['daytrip'] = $row['daytrip'];
|
||||
$item['season'] = $row['season'];
|
||||
$item['hideBookingButton'] = $row['hideBookingButton'];
|
||||
$item['servicesIncluded'] = unserialize($row['servicesIncluded'], ['allowed_classes' => false]);
|
||||
$item['new'] = $row['new'];
|
||||
$item['skiPassIncluded'] = $row['skiPassIncluded'];
|
||||
$item['teaserNights'] = [($row['nights'] ?? null)];
|
||||
$item['board'] = ($row['board'] ?? null);
|
||||
$item['boardUid'] = ($row['boardUid'] ?? null);
|
||||
$item['daytrip'] = ($row['daytrip'] ?? null);
|
||||
$item['season'] = ($row['season'] ?? null);
|
||||
$item['hideBookingButton'] = ($row['hideBookingButton'] ?? null);
|
||||
$item['servicesIncluded'] = unserialize((string)($row['servicesIncluded'] ?? ''), ['allowed_classes' => false]);
|
||||
$item['new'] = ($row['new'] ?? null);
|
||||
$item['skiPassIncluded'] = ($row['skiPassIncluded'] ?? null);
|
||||
}
|
||||
|
||||
// Availability indicators describe the consolidated set, not only
|
||||
// the row that supplies the teaser price.
|
||||
$item['earlybird'] = (bool) $item['earlybird'] || (bool) $row['earlybird'];
|
||||
$item['busAvailable'] = (bool) $item['busAvailable'] || (bool) $row['busAvailable'];
|
||||
$item['lowContingent'] = (bool) $item['lowContingent'] || (bool) $row['lowContingent'];
|
||||
$item['available'] = (bool) $item['available'] || (bool) $row['available'];
|
||||
$item['earlybird'] = (bool) $item['earlybird'] || (bool) ($row['earlybird'] ?? null);
|
||||
$item['busAvailable'] = (bool) $item['busAvailable'] || (bool) ($row['busAvailable'] ?? null);
|
||||
$item['lowContingent'] = (bool) $item['lowContingent'] || (bool) ($row['lowContingent'] ?? null);
|
||||
$item['available'] = (bool) $item['available'] || (bool) ($row['available'] ?? null);
|
||||
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ class SearchResultService implements SingletonInterface
|
||||
}
|
||||
|
||||
uasort($section['items'], function ($a, $b) {
|
||||
return strcmp($a['minPrice'], $b['minPrice']);
|
||||
return strcmp((string)$a['minPrice'], (string)$b['minPrice']);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ trait RequestArgumentTypeConversionTrait
|
||||
$searchParams = $searchResultService->getNormalizedSearchParams($searchParamsFromRequest);
|
||||
}
|
||||
|
||||
$this->request->setArgument('searchParams', $searchParams);
|
||||
$this->request = $this->request->withArgument('searchParams', $searchParams);
|
||||
}
|
||||
|
||||
public function processFilterSettingsArgument(array $forcedConceptUids = []): void
|
||||
@@ -42,7 +42,7 @@ trait RequestArgumentTypeConversionTrait
|
||||
$filterSettings = $this->filterService->getNormalizedFilterSettings($filterSettingsFromRequest, $forcedConceptUids);
|
||||
}
|
||||
|
||||
$this->request->setArgument('filterSettings', $filterSettings);
|
||||
$this->request = $this->request->withArgument('filterSettings', $filterSettings);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,15 @@ use EP\EpProducts\Middleware\ContentPageTypeGuardMiddleware;
|
||||
|
||||
return [
|
||||
'frontend' => [
|
||||
'ep/staticsearch' => [
|
||||
'target' => \EP\EpProducts\Middleware\StaticSearchParamsMiddleware::class,
|
||||
'before' => [
|
||||
'typo3/cms-frontend/page-resolver',
|
||||
],
|
||||
'after' => [
|
||||
'typo3/cms-frontend/site',
|
||||
],
|
||||
],
|
||||
'ep/productapi' => [
|
||||
'target' => ApiMiddleware::class,
|
||||
'before' => [
|
||||
|
||||
@@ -7,6 +7,12 @@ defined('TYPO3') or die();
|
||||
|
||||
call_user_func(function () {
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'searchresult',
|
||||
'Suchergebnis'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'product_detail',
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
use EP\EpProducts\Domain\Model\Hotel;
|
||||
|
||||
return [
|
||||
'ctrl' => [
|
||||
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_staticsearchparams',
|
||||
'label' => 'name',
|
||||
'prependAtCopy' => '[COPY %s]',
|
||||
'default_sortby' => 'ORDER BY name',
|
||||
'tstamp' => 'tstamp',
|
||||
'crdate' => 'crdate',
|
||||
'dividers2tabs' => true,
|
||||
'versioningWS' => true,
|
||||
'languageField' => 'sys_language_uid',
|
||||
'transOrigPointerField' => 'l10n_parent',
|
||||
'transOrigDiffSourceField' => 'l10n_diffsource',
|
||||
'delete' => 'deleted',
|
||||
'enablecolumns' => [
|
||||
'disabled' => 'hidden',
|
||||
'starttime' => 'starttime',
|
||||
'endtime' => 'endtime',
|
||||
],
|
||||
'searchFields' => 'name,path_segment',
|
||||
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
|
||||
],
|
||||
'types' => [
|
||||
'1' => [
|
||||
'showitem' => 'name, path_segment, country, region, concept, hotel, product, date_from,
|
||||
date_to, hotel_types, bus, pax',
|
||||
],
|
||||
],
|
||||
'palettes' => [],
|
||||
'columns' => [
|
||||
|
||||
'sys_language_uid' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
|
||||
'config' => ['type' => 'language'],
|
||||
],
|
||||
'l10n_parent' => [
|
||||
'displayCond' => 'FIELD:sys_language_uid:>:0',
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'default' => 0,
|
||||
'renderType' => 'selectSingle',
|
||||
'items' => [
|
||||
['label' => '', 'value' => 0],
|
||||
],
|
||||
'foreign_table' => 'tx_epproducts_domain_model_staticsearchparams',
|
||||
'foreign_table_where' => 'AND tx_epproducts_domain_model_staticsearchparams.pid=###CURRENT_PID### AND tx_epproducts_domain_model_staticsearchparams.sys_language_uid IN (-1,0)',
|
||||
],
|
||||
],
|
||||
'l10n_diffsource' => [
|
||||
'config' => [
|
||||
'type' => 'passthrough',
|
||||
],
|
||||
],
|
||||
|
||||
't3ver_label' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.versionLabel',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'size' => 30,
|
||||
'max' => 255,
|
||||
]
|
||||
],
|
||||
|
||||
'hidden' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_product.hidden',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
],
|
||||
],
|
||||
'starttime' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'size' => 13,
|
||||
'checkbox' => 0,
|
||||
'default' => 0,
|
||||
'behaviour' => [
|
||||
'allowLanguageSynchronization' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'endtime' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'size' => 13,
|
||||
'checkbox' => 0,
|
||||
'default' => 0,
|
||||
'behaviour' => [
|
||||
'allowLanguageSynchronization' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'bus' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Bus',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
],
|
||||
],
|
||||
'name' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Name',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'cols' => 30,
|
||||
'eval' => 'trim',
|
||||
'required' => true
|
||||
],
|
||||
],
|
||||
'path_segment' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'URL Segment',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'size' => 30,
|
||||
'eval' => 'trim,unique,alphanum_x,lower',
|
||||
'required' => true
|
||||
],
|
||||
],
|
||||
'concept' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Konzept',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingleBox',
|
||||
'foreign_table' => 'tx_epproducts_domain_model_concept',
|
||||
'foreign_table_where' => 'AND 1=1 ORDER BY name',
|
||||
'minItems' => 0,
|
||||
'maxitems' => 10,
|
||||
'size' => 5,
|
||||
'items' => [
|
||||
[ 'label' => 'Keine Auswahl', 'value' => 0 ],
|
||||
]
|
||||
],
|
||||
],
|
||||
'country' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Land',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'default' => 0,
|
||||
'renderType' => 'selectSingle',
|
||||
'foreign_table' => 'tx_epproducts_domain_model_country',
|
||||
'foreign_table_where' => 'AND 1=1 ORDER BY name',
|
||||
'minitems' => 0,
|
||||
'maxitems' => 1,
|
||||
'items' => [
|
||||
[ 'label' => 'Keine Auswahl', 'value' => 0 ],
|
||||
]
|
||||
],
|
||||
],
|
||||
'region' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Gebiet',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'default' => 0,
|
||||
'renderType' => 'selectSingle',
|
||||
'foreign_table' => 'tx_epproducts_domain_model_region',
|
||||
'foreign_table_where' => 'AND 1=1 ORDER BY name',
|
||||
'minitems' => 0,
|
||||
'maxitems' => 1,
|
||||
'items' => [
|
||||
[ 'label' => 'Keine Auswahl', 'value' => 0 ],
|
||||
]
|
||||
],
|
||||
],
|
||||
'hotel' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Hotel',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'default' => 0,
|
||||
'renderType' => 'selectSingle',
|
||||
'foreign_table' => 'tx_epproducts_domain_model_hotel',
|
||||
'foreign_table_where' => 'AND 1=1 ORDER BY tx_epproducts_domain_model_hotel.name',
|
||||
'minitems' => 0,
|
||||
'maxitems' => 1,
|
||||
'items' => [
|
||||
[ 'label' => 'Keine Auswahl', 'value' => 0 ],
|
||||
]
|
||||
],
|
||||
],
|
||||
'hotel_types' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Unterkunftsarten',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectCheckBox',
|
||||
'items' => [
|
||||
[ 'label' => 'Keine Auswahl', 'value' => 0 ],
|
||||
[ 'label' => 'Standard', 'value' => Hotel::CATEGORY_STANDARD ],
|
||||
[ 'label' => 'Komfort', 'value' => Hotel::CATEGORY_COMFORT ],
|
||||
[ 'label' => 'Deluxe', 'value' => Hotel::CATEGORY_DELUXE ],
|
||||
[ 'label' => 'Superdeluxe', 'value' => Hotel::CATEGORY_SUPER_DELUXE ],
|
||||
],
|
||||
'minitems' => 0,
|
||||
'maxitems' => 99,
|
||||
]
|
||||
],
|
||||
'date_from' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Datum von',
|
||||
'config' => [
|
||||
'dbType' => 'date',
|
||||
'type' => 'datetime',
|
||||
'size' => 7,
|
||||
'checkbox' => 0,
|
||||
'format' => 'date',
|
||||
]
|
||||
],
|
||||
'date_to' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Datum bis',
|
||||
'config' => [
|
||||
'dbType' => 'date',
|
||||
'type' => 'datetime',
|
||||
'size' => 7,
|
||||
'checkbox' => 0,
|
||||
'format' => 'date',
|
||||
]
|
||||
],
|
||||
'pax' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Personen',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'size' => 2,
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||
use EP\EpProducts\Controller\ProductController;
|
||||
use EP\EpProducts\Controller\SearchController;
|
||||
use EP\EpProducts\Controller\AjaxFilterPanelController;
|
||||
use EP\EpProducts\Controller\AjaxSearchController;
|
||||
use EP\EpProducts\Controller\AjaxTableController;
|
||||
use EP\EpProducts\Controller\AjaxCalendarController;
|
||||
use EP\EpProducts\Controller\AjaxTeaserPopupController;
|
||||
@@ -48,15 +51,30 @@ $boot = function () {
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'EpProducts',
|
||||
'searchresult',
|
||||
[
|
||||
SearchController::class => 'searchresult,processSearch',
|
||||
],
|
||||
[
|
||||
SearchController::class => 'searchresult,processSearch',
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'EpProducts',
|
||||
'ajax',
|
||||
[
|
||||
AjaxSearchController::class => 'searchresult',
|
||||
AjaxFilterPanelController::class => 'index',
|
||||
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
|
||||
AjaxCalendarController::class => 'priceCalendar,priceTable',
|
||||
AjaxTeaserPopupController::class => 'concept',
|
||||
],
|
||||
[
|
||||
AjaxSearchController::class => 'searchresult',
|
||||
AjaxFilterPanelController::class => 'index',
|
||||
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
|
||||
AjaxTeaserPopupController::class => 'concept',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpTheme\ViewHelpers;
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
* Copyright notice
|
||||
*
|
||||
* (c) 2021 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 TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
|
||||
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
|
||||
|
||||
class PriceRangeViewHelper extends AbstractViewHelper
|
||||
{
|
||||
public function initializeArguments()
|
||||
{
|
||||
parent::initializeArguments();
|
||||
|
||||
$this->registerArgument('min', 'mixed', 'Price range start');
|
||||
$this->registerArgument('max', 'mixed', 'Price range end');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $arguments
|
||||
* @param \Closure $renderChildrenClosure
|
||||
* @param RenderingContextInterface $renderingContext
|
||||
* @return mixed
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (1 === (int) $this->arguments['min']) {
|
||||
return sprintf('bis %d €', $this->arguments['max']);
|
||||
}
|
||||
if (999 === (int) $this->arguments['max']) {
|
||||
return sprintf('ab %d €', $this->arguments['min']);
|
||||
}
|
||||
return sprintf('%d bis %d €', $this->arguments['min'], $this->arguments['max']);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -39,7 +39,8 @@ export default class extends Controller {
|
||||
filter() {
|
||||
this.load({
|
||||
method: 'POST',
|
||||
body: new FormData(this.formTarget)
|
||||
// URL-encoded, not multipart: production rejects multipart requests (CVE-2026-109618)
|
||||
body: new URLSearchParams(new FormData(this.formTarget))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ export default class extends Controller {
|
||||
filter() {
|
||||
this.load({
|
||||
method: 'POST',
|
||||
body: new FormData(this.formTarget)
|
||||
// URL-encoded, not multipart: production rejects multipart requests (CVE-2026-109618)
|
||||
body: new URLSearchParams(new FormData(this.formTarget))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<aside>
|
||||
<f:render partial="ContactBar" arguments="{_all}"/>
|
||||
<f:render partial="Mobilenav" section="Main" arguments="{_all}"/>
|
||||
<f:render partial="Search/MobileSearch" arguments="{_all}"/>
|
||||
</aside>
|
||||
<f:render partial="Config" arguments="{_all}" />
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<f:render partial="Topnav" arguments="{_all}"/>
|
||||
<f:render partial="Mainnav" section="Main" arguments="{_all}"/>
|
||||
<main id="main-content">
|
||||
<f:render section="Content"/>
|
||||
</main>
|
||||
<footer>
|
||||
<f:render partial="Footer" arguments="{_all}"/>
|
||||
</footer>
|
||||
<aside>
|
||||
<f:render partial="ContactBar" arguments="{_all}"/>
|
||||
<f:render partial="Mobilenav" section="Main" arguments="{_all}"/>
|
||||
</aside>
|
||||
<f:render partial="Config" arguments="{_all}" />
|
||||
</html>
|
||||
@@ -17,5 +17,10 @@
|
||||
<f:render partial="ContactBar" arguments="{_all}"/>
|
||||
<f:render partial="Topnav" arguments="{_all}"/>
|
||||
<f:render partial="Mobilenav" section="Main" arguments="{_all}"/>
|
||||
<f:if condition="{data.tx_eptheme_hide_searchbar} || {settings.hideSearchbar}">
|
||||
<f:else>
|
||||
<f:render partial="Search/MobileSearch" arguments="{_all}"/>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:render partial="Config" arguments="{_all}" />
|
||||
</html>
|
||||
@@ -25,5 +25,6 @@
|
||||
<f:render partial="ContactBar" arguments="{_all}"/>
|
||||
<f:render partial="Topnav" arguments="{_all}"/>
|
||||
<f:render partial="Mobilenav" section="Main" arguments="{_all}"/>
|
||||
<f:render partial="Search/MobileSearch" arguments="{_all}"/>
|
||||
<f:render partial="Config" arguments="{_all}" />
|
||||
</html>
|
||||
@@ -93,6 +93,20 @@
|
||||
</f:else>
|
||||
</f:if>
|
||||
</div>
|
||||
<f:if condition="{teasers -> f:count()} > 3">
|
||||
<f:if condition="{settings.hideMoreLink}">
|
||||
<f:else>
|
||||
<div class="text-right mt-4">
|
||||
<f:link.typolink class="button bg-button"
|
||||
parameter="{settings.defaultSearchPageUid}"
|
||||
additionalParams="f={filterSettings -> ep:filterSettingsEncoder()}"
|
||||
additionalAttributes="{rel: 'nofollow'}">
|
||||
Alle anzeigen
|
||||
</f:link.typolink>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</f:if>
|
||||
</div>
|
||||
</f:if>
|
||||
<f:if condition="{city.descriptionExtended}">
|
||||
|
||||
@@ -98,6 +98,19 @@
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:if condition="{teasers -> f:count()} > 3">
|
||||
<f:if condition="{settings.hideMoreLink}">
|
||||
<f:else>
|
||||
<div class="text-right mt-4">
|
||||
<f:link.typolink class="button bg-button"
|
||||
parameter="{settings.defaultSearchPageUid}"
|
||||
additionalParams="f={filterSettings -> ep:filterSettingsEncoder()}">
|
||||
Alle anzeigen
|
||||
</f:link.typolink>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</f:if>
|
||||
</div>
|
||||
</f:then>
|
||||
<f:else>
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
<f:render section="FirstLevel" arguments="{_all}"/>
|
||||
</div>
|
||||
<div class="flex hover-hover:lg:hidden items-center space-x-4">
|
||||
<f:if condition="{settings.hideSearchbar}">
|
||||
<f:else>
|
||||
<button type="button"
|
||||
class="focus:outline-none text-zinc-800"
|
||||
aria-label="Reisen suchen"
|
||||
data-controller="overlay-toggle"
|
||||
data-overlay-toggle-id-value="mobile-search"
|
||||
data-action="overlay-toggle#invokeToggle">
|
||||
{ep:icon(icon: 'search', class: 'w-8 h-8 text-zinc-800')}
|
||||
</button>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<button type="button"
|
||||
class="focus:outline-none text-zinc-800"
|
||||
aria-label="Navigation öffnen"
|
||||
@@ -61,6 +73,27 @@
|
||||
</f:if>
|
||||
</li>
|
||||
</f:for>
|
||||
<f:if condition="{data.tx_eptheme_hide_searchbar} || {settings.hideSearchbar}">
|
||||
<f:else>
|
||||
<li class="pb-4" role="none">
|
||||
<f:if condition="{searchbarCollapsed}">
|
||||
<f:then>
|
||||
<button type="button"
|
||||
aria-label="Suche anzeigen/ausblenden"
|
||||
data-controller="searchbar-toggle"
|
||||
data-action="searchbar-toggle#toggle">
|
||||
{ep:icon(icon: 'search', class: 'w-6 h-6')}
|
||||
</button>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<f:link.typolink parameter="{settings.defaultSearchPageUid}" title="zur Suche" additionalAttributes="{'role': 'menuitem'}">
|
||||
{ep:icon(icon: 'search', class: 'w-6 h-6')}
|
||||
</f:link.typolink>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</li>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</ul>
|
||||
</nav>
|
||||
</f:section>
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
<f:image class="block h-10 lg:h-12 w-auto" src="{settings.logoImage}" alt="Bildmarke"/>
|
||||
</f:link.typolink>
|
||||
<div class="flex hover-hover:lg:hidden items-center space-x-4">
|
||||
<f:if condition="{data.tx_eptheme_hide_searchbar}">
|
||||
<f:else>
|
||||
<button type="button"
|
||||
class="focus:outline-none text-zinc-800"
|
||||
aria-label="Reisen suchen"
|
||||
data-controller="overlay-toggle"
|
||||
data-overlay-toggle-id-value="search"
|
||||
data-action="overlay-toggle#invokeToggle">
|
||||
{ep:icon(icon: 'search', class: 'w-8 h-8 text-zinc-800')}
|
||||
</button>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<button type="button"
|
||||
class="focus:outline-none"
|
||||
aria-label="Navigation schließen"
|
||||
|
||||
@@ -124,6 +124,19 @@
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:if condition="{teasers -> f:count()} > 3">
|
||||
<f:if condition="{settings.hideMoreLink}">
|
||||
<f:else>
|
||||
<div class="text-right mt-4">
|
||||
<f:link.typolink class="button bg-button"
|
||||
parameter="{settings.defaultSearchPageUid}"
|
||||
additionalParams="f={filterSettings -> ep:filterSettingsEncoder()}">
|
||||
Alle anzeigen
|
||||
</f:link.typolink>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</f:if>
|
||||
</div>
|
||||
</f:then>
|
||||
<f:else>
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<div class="mb-4"
|
||||
data-controller="collapse"
|
||||
data-collapse-until-value="960">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full bg-brand-gradient p-4">
|
||||
{ep:icon(icon: 'calendar', class: 'w-6 h-6 text-white mr-2')}
|
||||
<span class="flex-1 uppercase font-bold text-white text-left">
|
||||
Reisezeitraum
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<f:form.hidden property="dateFrom"
|
||||
data="{'search-target': 'dateFrom', 'mobile-search-target': 'dateFrom'}"/>
|
||||
<f:form.hidden property="dateTo"
|
||||
data="{'search-target': 'dateTo', 'mobile-search-target': 'dateTo'}"/>
|
||||
<div class="bg-brand-gradient p-4"
|
||||
data-controller="datepicker"
|
||||
data-datepicker-inline-value="false"
|
||||
data-datepicker-date-from-value="{filterSettings.dateFrom}"
|
||||
data-datepicker-date-to-value="{filterSettings.dateTo}"
|
||||
data-datepicker-min-date-value="{f:format.date(date: 'now', format: 'Y-m-d')}">
|
||||
<div class="mt-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="relative pb-2 md:pb-0">
|
||||
<input type="text"
|
||||
aria-label="Reisezeitraum"
|
||||
class="border-px border-white text-white text-xl p-2 w-full bg-transparent focus:border-white focus:ring-0"
|
||||
data-datepicker-target="field">
|
||||
{ep:icon(icon: 'calendar', class: 'w-6 h-6 text-white absolute top-0 right-0 translate-y-1/2 mr-2')}
|
||||
</div>
|
||||
<f:if condition="{settings.datePickerPresets}">
|
||||
<div class="flex flex-col divide-y divide-white/85 mt-2">
|
||||
<f:for each="{settings.datePickerPresets}" as="preset">
|
||||
<button type="button"
|
||||
class="flex items-center justify-between w-full text-white py-2"
|
||||
data-preset='{preset -> f:format.json() -> f:format.raw()}'
|
||||
data-action="datepicker#selectPreset">
|
||||
<span class="uppercase font-bold">{preset.label}</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6')}
|
||||
</button>
|
||||
</f:for>
|
||||
</div>
|
||||
</f:if>
|
||||
<button type="button"
|
||||
class="text-white text-left text-sm mt-2"
|
||||
data-action="datepicker#reset">
|
||||
zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4"
|
||||
data-controller="collapse"
|
||||
data-collapse-until-value="960">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full bg-brand-gradient p-4">
|
||||
{ep:icon(icon: 'concept', class: 'w-6 h-6 text-white mr-2')}
|
||||
<span class="flex-1 uppercase font-bold text-white text-left">
|
||||
Reisekonzepte
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container"
|
||||
class="p-4 border-t border-white/85 bg-brand-gradient">
|
||||
<ul class="mb-0 p-0 space-y-2">
|
||||
<f:for each="{filterOptions.concepts}" as="concept">
|
||||
<li>
|
||||
<label class="flex items-center" for="form-concept-{concept.uid}">
|
||||
<f:form.checkbox class="mr-2 rounded-full w-5 h-5 border-white ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
id="form-concept-{concept.uid}"
|
||||
property="conceptUids"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{concept.uid}"/>
|
||||
<span class="block w-full py-1 px-2 bg-concept-{concept.code} text-white text-lg rounded">{concept.name}</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:if condition="{filterOptions.resultsUri}">
|
||||
<div class="my-4">
|
||||
<a href="{filterOptions.resultsUri}"
|
||||
class="button text-white bg-badge-gradient w-full">
|
||||
{filterOptions.resultCount} Treffer anzeigen
|
||||
</a>
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
<div class="divide-y py-2 px-4 hover-hover:lg:bg-white">
|
||||
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.countryUids} || {filterSettings.regionUids}', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Reiseziel
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4">
|
||||
<f:for each="{filterOptions.destinations}" as="destination">
|
||||
<li class="relative border-b last:border-0 mb-2">
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="countryUids"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{destination.country.uid}"/>
|
||||
<span class="pl-2 leading-none">
|
||||
<strong>{destination.country.label}</strong>
|
||||
</span>
|
||||
{ep:icon(icon: 'flag-{destination.country.code -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6 absolute top-0 right-0')}
|
||||
</label>
|
||||
<f:if condition="{destination.regions}">
|
||||
<ul class="py-2 space-y-2">
|
||||
<f:for each="{destination.regions}" as="region">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="regionUids"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{region.uid}"/>
|
||||
<span class="leading-none pl-2">
|
||||
{region.label}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</f:if>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.priceRanges}', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Preis
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||
<f:for each="{filterOptions.priceRanges}" as="range">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="priceRanges"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{range.uid}"/>
|
||||
<span class="leading-none pl-2">
|
||||
{ep:priceRange(min: range.min, max: range.max)}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.pax} > 1', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Personen
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<f:form.select class="w-full border-0 focus:ring-0 bg-right bg-transparent text-white lg:bg-white lg:text-zinc-800 mt-2 p-0"
|
||||
property="pax"
|
||||
options="{filterOptions.pax}"
|
||||
data="{action: '{stimulusController}#filter'}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:variable name="hotelTypes" value="{
|
||||
1: 'Ferienwohnung',
|
||||
2: 'Sportclub',
|
||||
3: 'Appartment',
|
||||
4: 'Gästehaus',
|
||||
5: 'Pension'
|
||||
}"/>
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.hotelTypes}', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Unterkunft
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||
<f:for each="{filterOptions.hotelTypes}" as="type">
|
||||
<f:if condition="{hotelTypes.{type}}">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="hotelTypes"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{type}"/>
|
||||
<span class="leading-none pl-2">
|
||||
{hotelTypes.{type}}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:if>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.boardTypes}', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Verpflegung
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||
<f:for each="{filterOptions.boardTypes}" as="type">
|
||||
<f:if condition="{type.label}">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="boardTypes"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{type.uid}"/>
|
||||
<span class="pl-2 leading-none">
|
||||
{type.label}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:if>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:variable name="roomTypes" value="{
|
||||
1: 'EZ',
|
||||
2: 'DZ / 2er',
|
||||
3: '3er',
|
||||
4: '4er',
|
||||
5: '5er',
|
||||
6: '6er',
|
||||
7: '7er',
|
||||
8: '8er',
|
||||
9: '9er und mehr',
|
||||
10: 'mit anderen'
|
||||
}"/>
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.roomTypes}', then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Zimmerarten/Apart.
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6 hover-none:text-white', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||
<f:for each="{filterOptions.roomTypes}" as="type">
|
||||
<f:if condition="{roomTypes.{type}}">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="roomTypes"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="{type}"/>
|
||||
<span class="leading-none pl-2">
|
||||
{roomTypes.{type}}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</f:if>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:if condition="{filterOptions.busAvailable}">
|
||||
<div class="py-2"
|
||||
data-controller="collapse"
|
||||
data-collapse-collapsed-value="{f:if(condition: filterSettings.bus, then: 'false', else: 'true')}">
|
||||
<button type="button"
|
||||
data-action="click->collapse#toggle"
|
||||
class="flex items-center justify-between w-full">
|
||||
<span class="uppercase font-bold">
|
||||
Anreise
|
||||
</span>
|
||||
{ep:icon(icon: 'arrow-right', class: 'w-6 h-6', data: '{collapse-target: \'icon\'}')}
|
||||
</button>
|
||||
<div data-collapse-target="container">
|
||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||
<li>
|
||||
<label class="flex items-start">
|
||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||
property="bus"
|
||||
data="{action: '{stimulusController}#filter'}"
|
||||
value="1"/>
|
||||
<span class="leading-none pl-2">
|
||||
Busanreise
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div class="col-span-3 lg:col-span-1 relative">
|
||||
<div class="hidden lg:block">
|
||||
<h3 class="headline headline--underlined mb-4">
|
||||
Filter
|
||||
</h3>
|
||||
<f:form object="{filterSettings}"
|
||||
name="filterSettings"
|
||||
pluginName="Ajax"
|
||||
data="{'search-target': 'form'}">
|
||||
<f:render partial="Search/FilterPanel" arguments="{_all}"/>
|
||||
</f:form>
|
||||
<div class="mt-4">
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--secondary w-full"
|
||||
data-action="search#reset">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3 lg:col-span-2 relative">
|
||||
<h3 class="headline headline--underlined mb-4">
|
||||
Suchergebnis
|
||||
</h3>
|
||||
<f:render partial="Search/Result" arguments="{_all}"/>
|
||||
</div>
|
||||
<f:render partial="Search/MobileSearch" arguments="{_all}"/>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<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">
|
||||
|
||||
<div class="fixed hover-hover:lg:hidden top-0 left-0 inset-0 w-screen h-screen pt-14 hover-none:lg:pt-20 pb-10 z-50 bg-zinc-700 hidden"
|
||||
data-controller="mobile-search"
|
||||
data-mobile-search-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'index', pageUid: settings.defaultAjaxUid, format: 'html')}"
|
||||
data-mobile-search-init-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'index', pageUid: settings.defaultAjaxUid, arguments: '{filterSettings: filterSettings}', format: 'html')}"
|
||||
data-action="overlay-toggle:toggle@window->mobile-search#open datepicker:range-select->mobile-search#selectDateRange">
|
||||
<div class="fixed top-0 left-0 z-100 inset-x-0 px-4 py-2 hover-none:lg:py-4 flex justify-between bg-white shadow-md">
|
||||
<a href="{f:uri.typolink(parameter: settings.defaultHomeUid)}"
|
||||
class="block hover-hover:lg:pb-4"
|
||||
title="zur Startseite">
|
||||
<f:image class="block h-10 lg:h-12 w-auto" src="{settings.logoImage}" alt="Bildmarke"/>
|
||||
</a>
|
||||
<button type="button"
|
||||
aria-label="Suche schließen"
|
||||
class="focus:outline-none" data-action="mobile-search#close">
|
||||
{ep:icon(icon: 'close', class: 'w-8 h-8')}
|
||||
</button>
|
||||
</div>
|
||||
<div data-mobile-search-target="container"
|
||||
class="h-full w-full overflow-y-scroll px-4 pt-4 pb-24 bg-ep-primary-dark text-white">
|
||||
</div>
|
||||
<div class="fixed top-0 left-0 z-20 w-full h-full hidden"
|
||||
data-mobile-search-target="loadingIndicator">
|
||||
<div class="absolute top-1/2 left-1/2 -translate-xy-1/2 flex items-center space-x-4 bg-ep-secondary text-white text-lg p-4 rounded">
|
||||
{ep:icon(icon: 'spinner', class: 'w-6 h-6')}
|
||||
<span>Suche wird geladen...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,114 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
|
||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers">
|
||||
|
||||
<div class="flex mb-4 border-2 rounded-sm {f:if(condition: '{meta.organic} && {meta.total} > 0', then: 'border-emerald-600', else: 'border-red-600')}">
|
||||
<div class="flex flex-col items-center justify-center px-2 {f:if(condition: '{meta.organic} && {meta.total} > 0', then: 'bg-emerald-600', else: 'bg-red-600')}">
|
||||
<f:if condition="{meta.organic} && {meta.total} > 0">
|
||||
<f:then>
|
||||
{ep:icon(icon: 'emoji-happy', class: 'w-6 h-6 text-white')}
|
||||
</f:then>
|
||||
<f:else>
|
||||
{ep:icon(icon: 'emoji-sad', class: 'w-6 h-6 text-white')}
|
||||
</f:else>
|
||||
</f:if>
|
||||
</div>
|
||||
<div class="flex-1 px-4 py-2 bg-white text-lg">
|
||||
Deine Suche
|
||||
<f:if condition="{meta.organic}">
|
||||
<f:then>
|
||||
<f:if condition="{meta.destinationName}">
|
||||
nach <strong>{meta.destinationName}</strong>
|
||||
</f:if>
|
||||
<f:if condition="{meta.dateRange}">
|
||||
<strong>{meta.dateRange}</strong>
|
||||
</f:if>
|
||||
ergab {meta.total} Treffer
|
||||
</f:then>
|
||||
<f:else>
|
||||
ergab leider keine genauen Treffer. Bitte benutze die Filter.
|
||||
</f:else>
|
||||
</f:if>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="lg:hidden mb-4 text-white bg-badge-gradient uppercase py-2 w-full"
|
||||
data-controller="overlay-toggle"
|
||||
data-overlay-toggle-id-value="mobile-search"
|
||||
data-action="overlay-toggle#invokeToggle">
|
||||
Filter
|
||||
</button>
|
||||
<f:for each="{searchResult}" as="section">
|
||||
<div id="concept-{section.concept.uid}">
|
||||
<h4 class="headline headline--tertiary headline--underlined">
|
||||
{section.concept.name}
|
||||
</h4>
|
||||
<div class="pb-4 md:pb-8"
|
||||
data-controller="teasergrid"
|
||||
data-teasergrid-grid-class="grid-cols-2"
|
||||
data-teasergrid-disabled-value="false">
|
||||
<div class="flex items-center space-x-1 mb-2 sm:hidden">
|
||||
<button type="button"
|
||||
class="opacity-50"
|
||||
aria-label="kompakte Ansicht"
|
||||
data-action="teasergrid#grid"
|
||||
data-teasergrid-target="button">
|
||||
{ep:icon(icon: 'teaser-grid', class: 'w-5 h-5 text-ep-primary')}
|
||||
</button>
|
||||
<button type="button"
|
||||
aria-label="vergrößerte Ansicht"
|
||||
data-action="teasergrid#block"
|
||||
data-teasergrid-target="button">
|
||||
{ep:icon(icon: 'teaser-single', class: 'w-5 h-5 text-ep-primary')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 xl:grid-cols-3 gap-4 md:gap-8"
|
||||
data-teasergrid-target="container">
|
||||
<f:for each="{section.items}" as="item">
|
||||
<f:if condition="{section.concept.code} == 'evt'">
|
||||
<f:then>
|
||||
<f:variable name="searchProductImage" value="{item.product.images.original.0}"/>
|
||||
<f:variable name="searchForceHotelUid" value="0"/>
|
||||
<f:variable name="searchShowHotelDetails" value="0"/>
|
||||
<f:variable name="searchHideWatchlist" value="1"/>
|
||||
<f:variable name="searchDateFrom" value=""/>
|
||||
<f:variable name="searchDateTo" value=""/>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<f:variable name="searchProductImage" value="{item.hotel.images.original.0}"/>
|
||||
<f:variable name="searchForceHotelUid" value="1"/>
|
||||
<f:variable name="searchShowHotelDetails" value="1"/>
|
||||
<f:variable name="searchHideWatchlist" value="0"/>
|
||||
<f:variable name="searchDateFrom" value="{item.minDateStart}"/>
|
||||
<f:variable name="searchDateTo" value="{item.maxDateEnd}"/>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:render partial="Product/Teaser" section="Teaser" arguments="{
|
||||
concept: section.concept,
|
||||
hotel: item.hotel,
|
||||
product: item.product,
|
||||
country: item.country,
|
||||
region: item.region,
|
||||
productImage: searchProductImage,
|
||||
nights: item.teaserNights,
|
||||
board: item.board,
|
||||
minPrice: item.minPrice,
|
||||
daytrip: item.daytrip,
|
||||
season: item.season,
|
||||
dateFrom: searchDateFrom,
|
||||
dateTo: searchDateTo,
|
||||
forceHotelUid: searchForceHotelUid,
|
||||
showHotelDetails: searchShowHotelDetails,
|
||||
hideWatchlist: searchHideWatchlist,
|
||||
new: item.new,
|
||||
skiPassIncluded: item.skiPassIncluded,
|
||||
referringPageUri: referringPageUri
|
||||
}"/>
|
||||
</f:for>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</f:for>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<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">
|
||||
<f:form object="{filterSettings}"
|
||||
name="filterSettings"
|
||||
pluginName="Ajax"
|
||||
action=""
|
||||
data="{'mobile-search-target': 'form'}">
|
||||
<f:render partial="Search/FilterPanel" arguments="{_all}"/>
|
||||
</f:form>
|
||||
<f:if condition="{filterOptions.resultsUri}">
|
||||
<div class="mt-4">
|
||||
<a href="{filterOptions.resultsUri}"
|
||||
class="button text-white bg-badge-gradient w-full">
|
||||
{filterOptions.resultCount} Treffer anzeigen
|
||||
</a>
|
||||
</div>
|
||||
</f:if>
|
||||
<div class="mt-4">
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--secondary w-full"
|
||||
data-action="mobile-search#reset">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<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">
|
||||
<f:render partial="Search/Main" arguments="{_all}"/>
|
||||
</f:section>
|
||||
|
||||
<f:section name="SearchResult">
|
||||
<f:render partial="Search/Result" arguments="{_all}"/>
|
||||
</f:section>
|
||||
|
||||
<f:section name="FilterPanel">
|
||||
<f:render partial="Search/FilterPanel" arguments="{_all}"/>
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<f:layout name="Page/Search"/>
|
||||
|
||||
<f:section name="Content">
|
||||
<f:cObject typoscriptObjectPath="lib.dynamicContent" data="{pageUid: '{data.uid}', colPos: '0'}" />
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
@@ -89,6 +89,19 @@
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:if condition="{teasers -> f:count()} > 3">
|
||||
<f:if condition="{settings.hideMoreLink}">
|
||||
<f:else>
|
||||
<div class="text-right mt-4">
|
||||
<f:link.typolink class="button bg-button"
|
||||
parameter="{settings.defaultSearchPageUid}"
|
||||
additionalParams="f={filterSettings -> ep:filterSettingsEncoder()}">
|
||||
Alle anzeigen
|
||||
</f:link.typolink>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</f:if>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<f:if condition="{nonBookableInfo}">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<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">
|
||||
<div class="pb-8 md:py-8 bg-ep-primary-bg">
|
||||
<div class="container"
|
||||
data-controller="search"
|
||||
data-search-uri-value="{ep:uri.ajax(controller: 'AjaxSearch', action: 'searchresult', pageUid: settings.defaultAjaxUid, format: 'html')}"
|
||||
data-action="datepicker:range-select->search#selectDateRange">
|
||||
<div data-search-target="resultsContainer">
|
||||
<f:render partial="Search/Main" arguments="{_all}"/>
|
||||
</div>
|
||||
<div class="fixed top-0 left-0 z-20 w-screen h-screen hidden"
|
||||
data-search-target="loadingIndicator">
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 flex items-center space-x-4 bg-badge-gradient text-white text-lg p-4 rounded">
|
||||
<svg class="w-6 h-6">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-spinner"></use>
|
||||
</svg>
|
||||
<span>Suchergebnis wird geladen...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user