diff --git a/config/sites/ski-wochenenden/config.yaml b/config/sites/ski-wochenenden/config.yaml index ab2a219..6a7479e 100644 --- a/config/sites/ski-wochenenden/config.yaml +++ b/config/sites/ski-wochenenden/config.yaml @@ -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' diff --git a/packages/ep_products/Classes/Controller/AjaxFilterPanelController.php b/packages/ep_products/Classes/Controller/AjaxFilterPanelController.php new file mode 100644 index 0000000..1ec7488 --- /dev/null +++ b/packages/ep_products/Classes/Controller/AjaxFilterPanelController.php @@ -0,0 +1,101 @@ +, 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(); + } + +} diff --git a/packages/ep_products/Classes/Controller/AjaxSearchController.php b/packages/ep_products/Classes/Controller/AjaxSearchController.php new file mode 100644 index 0000000..e8dc32b --- /dev/null +++ b/packages/ep_products/Classes/Controller/AjaxSearchController.php @@ -0,0 +1,133 @@ +, 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(); + } + +} diff --git a/packages/ep_products/Classes/Controller/AjaxTableController.php b/packages/ep_products/Classes/Controller/AjaxTableController.php index afd71e4..9e23dcd 100644 --- a/packages/ep_products/Classes/Controller/AjaxTableController.php +++ b/packages/ep_products/Classes/Controller/AjaxTableController.php @@ -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); } } diff --git a/packages/ep_products/Classes/Controller/SearchController.php b/packages/ep_products/Classes/Controller/SearchController.php new file mode 100644 index 0000000..0c89099 --- /dev/null +++ b/packages/ep_products/Classes/Controller/SearchController.php @@ -0,0 +1,169 @@ +, 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(); + } +} diff --git a/packages/ep_products/Classes/Domain/Model/Dto/FilterOptions.php b/packages/ep_products/Classes/Domain/Model/Dto/FilterOptions.php index 1b95aba..ffcb801 100644 --- a/packages/ep_products/Classes/Domain/Model/Dto/FilterOptions.php +++ b/packages/ep_products/Classes/Domain/Model/Dto/FilterOptions.php @@ -325,7 +325,7 @@ class FilterOptions implements \JsonSerializable /** * @return array */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return [ 'priceRanges' => $this->getPriceRanges(), diff --git a/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php b/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php index d3ed3a0..20470f3 100644 --- a/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php +++ b/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php @@ -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) diff --git a/packages/ep_products/Classes/Middleware/StaticSearchParamsMiddleware.php b/packages/ep_products/Classes/Middleware/StaticSearchParamsMiddleware.php new file mode 100644 index 0000000..816d8fd --- /dev/null +++ b/packages/ep_products/Classes/Middleware/StaticSearchParamsMiddleware.php @@ -0,0 +1,71 @@ +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); + } +} diff --git a/packages/ep_products/Classes/Service/FilterService.php b/packages/ep_products/Classes/Service/FilterService.php index ffbfba6..1b44591 100644 --- a/packages/ep_products/Classes/Service/FilterService.php +++ b/packages/ep_products/Classes/Service/FilterService.php @@ -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; } diff --git a/packages/ep_products/Classes/Service/SearchResultService.php b/packages/ep_products/Classes/Service/SearchResultService.php index 768097c..268cfe2 100644 --- a/packages/ep_products/Classes/Service/SearchResultService.php +++ b/packages/ep_products/Classes/Service/SearchResultService.php @@ -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']); }); } diff --git a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php index 9104d9b..d03fbbb 100644 --- a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php +++ b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php @@ -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); } } diff --git a/packages/ep_products/Configuration/RequestMiddlewares.php b/packages/ep_products/Configuration/RequestMiddlewares.php index 7d8d1b4..73016e7 100644 --- a/packages/ep_products/Configuration/RequestMiddlewares.php +++ b/packages/ep_products/Configuration/RequestMiddlewares.php @@ -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' => [ diff --git a/packages/ep_products/Configuration/TCA/Overrides/tt_content.php b/packages/ep_products/Configuration/TCA/Overrides/tt_content.php index 8bad90d..e93d057 100644 --- a/packages/ep_products/Configuration/TCA/Overrides/tt_content.php +++ b/packages/ep_products/Configuration/TCA/Overrides/tt_content.php @@ -7,6 +7,12 @@ defined('TYPO3') or die(); call_user_func(function () { + ExtensionUtility::registerPlugin( + 'EpProducts', + 'searchresult', + 'Suchergebnis' + ); + ExtensionUtility::registerPlugin( 'EpProducts', 'product_detail', diff --git a/packages/ep_products/Configuration/TCA/tx_epproducts_domain_model_staticsearchparams.php b/packages/ep_products/Configuration/TCA/tx_epproducts_domain_model_staticsearchparams.php new file mode 100644 index 0000000..f5c0aa7 --- /dev/null +++ b/packages/ep_products/Configuration/TCA/tx_epproducts_domain_model_staticsearchparams.php @@ -0,0 +1,243 @@ + [ + '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, + ] + ] + ], +]; diff --git a/packages/ep_products/ext_localconf.php b/packages/ep_products/ext_localconf.php index a699a50..72d509a 100644 --- a/packages/ep_products/ext_localconf.php +++ b/packages/ep_products/ext_localconf.php @@ -1,6 +1,9 @@ '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', ] diff --git a/packages/ep_theme/Classes/ViewHelpers/PriceRangeViewHelper.php b/packages/ep_theme/Classes/ViewHelpers/PriceRangeViewHelper.php new file mode 100644 index 0000000..79f4473 --- /dev/null +++ b/packages/ep_theme/Classes/ViewHelpers/PriceRangeViewHelper.php @@ -0,0 +1,59 @@ +, 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']); + } +} diff --git a/packages/ep_theme/Resources/Private/Assets/js/controllers/mobile_search_controller.js b/packages/ep_theme/Resources/Private/Assets/js/controllers/mobile_search_controller.js index aea506f..4ef29b5 100644 --- a/packages/ep_theme/Resources/Private/Assets/js/controllers/mobile_search_controller.js +++ b/packages/ep_theme/Resources/Private/Assets/js/controllers/mobile_search_controller.js @@ -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)) }) } diff --git a/packages/ep_theme/Resources/Private/Assets/js/controllers/search_controller.js b/packages/ep_theme/Resources/Private/Assets/js/controllers/search_controller.js index 8132fb1..212ea2f 100644 --- a/packages/ep_theme/Resources/Private/Assets/js/controllers/search_controller.js +++ b/packages/ep_theme/Resources/Private/Assets/js/controllers/search_controller.js @@ -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)) }) } diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html index f4a364e..64be84e 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html @@ -14,6 +14,7 @@ diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Search.html b/packages/ep_theme/Resources/Private/Layouts/Page/Search.html new file mode 100644 index 0000000..2a564bd --- /dev/null +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Search.html @@ -0,0 +1,18 @@ + + + + +
+ +
+
+ +
+ + + diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html index 7660c80..6317c28 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html @@ -17,5 +17,10 @@ + + + + + diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html index 9acb4ea..41b820f 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html @@ -25,5 +25,6 @@ + diff --git a/packages/ep_theme/Resources/Private/Partials/City/Details.html b/packages/ep_theme/Resources/Private/Partials/City/Details.html index 8944b2d..d6a79a6 100644 --- a/packages/ep_theme/Resources/Private/Partials/City/Details.html +++ b/packages/ep_theme/Resources/Private/Partials/City/Details.html @@ -93,6 +93,20 @@ + + + +
+ + Alle anzeigen + +
+
+
+
diff --git a/packages/ep_theme/Resources/Private/Partials/Hotel/Details.html b/packages/ep_theme/Resources/Private/Partials/Hotel/Details.html index f9cd886..45cba2f 100644 --- a/packages/ep_theme/Resources/Private/Partials/Hotel/Details.html +++ b/packages/ep_theme/Resources/Private/Partials/Hotel/Details.html @@ -98,6 +98,19 @@ + + + +
+ + Alle anzeigen + +
+
+
+
diff --git a/packages/ep_theme/Resources/Private/Partials/Mainnav.html b/packages/ep_theme/Resources/Private/Partials/Mainnav.html index d4fc881..52cfadd 100644 --- a/packages/ep_theme/Resources/Private/Partials/Mainnav.html +++ b/packages/ep_theme/Resources/Private/Partials/Mainnav.html @@ -17,6 +17,18 @@
+ + + + + + + + + {ep:icon(icon: 'search', class: 'w-6 h-6')} + + + + + + diff --git a/packages/ep_theme/Resources/Private/Partials/Mobilenav.html b/packages/ep_theme/Resources/Private/Partials/Mobilenav.html index 4c2c42b..7c00a5d 100644 --- a/packages/ep_theme/Resources/Private/Partials/Mobilenav.html +++ b/packages/ep_theme/Resources/Private/Partials/Mobilenav.html @@ -17,6 +17,18 @@
+ + + + +
diff --git a/packages/ep_theme/Resources/Private/Partials/Search/FilterPanel.html b/packages/ep_theme/Resources/Private/Partials/Search/FilterPanel.html new file mode 100644 index 0000000..99e1d3a --- /dev/null +++ b/packages/ep_theme/Resources/Private/Partials/Search/FilterPanel.html @@ -0,0 +1,348 @@ + + +
+ +
+ + +
+
+
+
+ + {ep:icon(icon: 'calendar', class: 'w-6 h-6 text-white absolute top-0 right-0 translate-y-1/2 mr-2')} +
+ +
+ + + +
+
+ +
+
+
+
+
+ +
+ +
+
    + +
  • + +
  • +
    +
+
+
+ + + + + +
+ +
+ +
+
    + +
  • + + +
      + +
    • + +
    • +
      +
    +
    +
  • +
    +
+
+
+ +
+ +
+
    + +
  • + +
  • +
    +
+
+
+ +
+ +
+ +
+
+ + +
+ +
+
    + + +
  • + +
  • +
    +
    +
+
+
+ +
+ +
+
    + + +
  • + +
  • +
    +
    +
+
+
+ + +
+ +
+
    + + +
  • + +
  • +
    +
    +
+
+
+ + +
+ +
+
    +
  • + +
  • +
+
+
+
+ +
+ + diff --git a/packages/ep_theme/Resources/Private/Partials/Search/Main.html b/packages/ep_theme/Resources/Private/Partials/Search/Main.html new file mode 100644 index 0000000..01ce3c3 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Partials/Search/Main.html @@ -0,0 +1,35 @@ + + +
+
+ +
+
+

+ Suchergebnis +

+ +
+ +
+ + diff --git a/packages/ep_theme/Resources/Private/Partials/Search/MobileSearch.html b/packages/ep_theme/Resources/Private/Partials/Search/MobileSearch.html new file mode 100644 index 0000000..193b9fa --- /dev/null +++ b/packages/ep_theme/Resources/Private/Partials/Search/MobileSearch.html @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/ep_theme/Resources/Private/Partials/Search/Result.html b/packages/ep_theme/Resources/Private/Partials/Search/Result.html new file mode 100644 index 0000000..348b16a --- /dev/null +++ b/packages/ep_theme/Resources/Private/Partials/Search/Result.html @@ -0,0 +1,114 @@ + + +
+
+ + + {ep:icon(icon: 'emoji-happy', class: 'w-6 h-6 text-white')} + + + {ep:icon(icon: 'emoji-sad', class: 'w-6 h-6 text-white')} + + +
+
+ Deine Suche + + + + nach {meta.destinationName} + + + {meta.dateRange} + + ergab {meta.total} Treffer + + + ergab leider keine genauen Treffer. Bitte benutze die Filter. + + +
+
+ + +
+

+ {section.concept.name} +

+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + diff --git a/packages/ep_theme/Resources/Private/Templates/AjaxFilterPanel/Index.html b/packages/ep_theme/Resources/Private/Templates/AjaxFilterPanel/Index.html new file mode 100644 index 0000000..9aad6b6 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/AjaxFilterPanel/Index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + +
+ +
+
+ + diff --git a/packages/ep_theme/Resources/Private/Templates/AjaxSearch/Searchresult.html b/packages/ep_theme/Resources/Private/Templates/AjaxSearch/Searchresult.html new file mode 100644 index 0000000..6c17236 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/AjaxSearch/Searchresult.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Page/Search.html b/packages/ep_theme/Resources/Private/Templates/Page/Search.html new file mode 100644 index 0000000..1001ea8 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Page/Search.html @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Product/Teasergroup.html b/packages/ep_theme/Resources/Private/Templates/Product/Teasergroup.html index 1e739e4..0205370 100644 --- a/packages/ep_theme/Resources/Private/Templates/Product/Teasergroup.html +++ b/packages/ep_theme/Resources/Private/Templates/Product/Teasergroup.html @@ -89,6 +89,19 @@
+ + + +
+ + Alle anzeigen + +
+
+
+
diff --git a/packages/ep_theme/Resources/Private/Templates/Search/Searchresult.html b/packages/ep_theme/Resources/Private/Templates/Search/Searchresult.html new file mode 100644 index 0000000..c5c760d --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Search/Searchresult.html @@ -0,0 +1,29 @@ + + + + + +
+
+
+ +
+ +
+
+
+ +