diff --git a/config/sites/ep-reisen/config.yaml b/config/sites/ep-reisen/config.yaml index 8d85bbb..f6acfe2 100644 --- a/config/sites/ep-reisen/config.yaml +++ b/config/sites/ep-reisen/config.yaml @@ -284,6 +284,8 @@ routeEnhancers: /html: 1702 /form: 1704 /captcha: 3413 + searchbar: 1706 + searchbar-collapsed: 1707 /snowreport: 1705 .json: 1803 .ical: 1710 diff --git a/packages/ep_products/Classes/Controller/SearchController.php b/packages/ep_products/Classes/Controller/SearchController.php index 0c89099..2bc11f8 100644 --- a/packages/ep_products/Classes/Controller/SearchController.php +++ b/packages/ep_products/Classes/Controller/SearchController.php @@ -40,6 +40,12 @@ class SearchController extends ActionController use RequestArgumentTypeConversionTrait; + /** + * The fields of the search bar (Templates/Searchbar/Index.html), sent by GET to the search + * page and excluded from the cHash: the visitor chooses the values + */ + public const SEARCHBAR_PARAMETERS = ['dateFrom', 'dateTo', 'pax', 'destinationType', 'destinationUid', 'priceRange']; + /** * @var FilterService */ @@ -59,32 +65,6 @@ class SearchController extends ActionController $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. @@ -98,6 +78,24 @@ class SearchController extends ActionController $this->request = $this->request->withArgument('filterSettings', $filterSettings); } } + // The fields of the search bar, which sends them here by GET + $searchbarParameters = array_filter( + array_intersect_key($this->request->getQueryParams(), array_flip(self::SEARCHBAR_PARAMETERS)), + 'is_string' + ); + // Anything but a real Y-m-d date is dropped: the URL is easy to edit, and + // DateUtility::formatDateRange() throws on what DateTime cannot read + foreach (['dateFrom', 'dateTo'] as $name) { + $date = \DateTime::createFromFormat('!Y-m-d', $searchbarParameters[$name] ?? ''); + if (!$date || $date->format('Y-m-d') !== $searchbarParameters[$name]) { + unset($searchbarParameters[$name]); + } + } + if ($searchbarParameters !== [] && !$this->request->hasArgument('filterSettings')) { + $searchParams = $this->searchResultService->getNormalizedSearchParams($searchbarParameters); + $filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams); + $this->request = $this->request->withArgument('filterSettings', $filterSettings); + } $forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true); $this->processFilterSettingsArgument($forcedConceptUids); } diff --git a/packages/ep_products/Classes/Controller/SearchbarController.php b/packages/ep_products/Classes/Controller/SearchbarController.php new file mode 100644 index 0000000..7c7e671 --- /dev/null +++ b/packages/ep_products/Classes/Controller/SearchbarController.php @@ -0,0 +1,66 @@ +, 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 EP\EpProducts\Service\FilterService; +use EP\EpProducts\Service\SearchResultService; +use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; + +/** + * The search bar in the page header. It takes no arguments: the options come from the products + * the site can show, so the output is the same on every page of a site (cacheable), and the + * static pages of the hybrid setup load it from /searchbar (Page/Searchbar.typoscript). + * The form sends its fields by GET to the search page (SearchController::SEARCHBAR_PARAMETERS). + */ +class SearchbarController extends ActionController +{ + public function __construct( + private readonly FilterService $filterService, + private readonly SearchResultService $searchResultService, + ) { + } + + public function indexAction(): ResponseInterface + { + $searchParams = $this->searchResultService->getNormalizedSearchParams(); + $forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'] ?? '', true); + $filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams, $forcedConceptUids); + $excludedConceptUids = GeneralUtility::intExplode(',', $this->settings['excludedConceptUids'] ?? '', true); + + $searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids); + + $this->view->assignMultiple([ + 'filterOptions' => $this->filterService->collectFilterOptionsFromSearchResult($searchResult), + 'searchParams' => $searchParams, + 'collapsed' => (bool)($this->settings['collapsed'] ?? false), + ]); + return $this->htmlResponse(); + } +} diff --git a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php index d03fbbb..62bc0a7 100644 --- a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php +++ b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php @@ -3,30 +3,11 @@ namespace EP\EpProducts\Traits; use EP\EpProducts\Service\FilterService; -use EP\EpProducts\Service\SearchResultService; use TYPO3\CMS\Core\Utility\GeneralUtility; trait RequestArgumentTypeConversionTrait { - public function processSearchParamsArgument(): void - { - - /** @var SearchResultService $searchResultService */ - $searchResultService = GeneralUtility::makeInstance(SearchResultService::class); - - if (!$this->request->hasArgument('searchParams')) { - $searchParams = $searchResultService->getNormalizedSearchParams(); - } else { - $requestArgument = $this->request->getArgument('searchParams'); - $searchParamsFromRequest = is_array($requestArgument) ? - $requestArgument : json_decode($requestArgument, true, 512, JSON_THROW_ON_ERROR); - $searchParams = $searchResultService->getNormalizedSearchParams($searchParamsFromRequest); - } - - $this->request = $this->request->withArgument('searchParams', $searchParams); - } - public function processFilterSettingsArgument(array $forcedConceptUids = []): void { diff --git a/packages/ep_products/ext_localconf.php b/packages/ep_products/ext_localconf.php index c6c21d6..1bd9fe9 100644 --- a/packages/ep_products/ext_localconf.php +++ b/packages/ep_products/ext_localconf.php @@ -2,6 +2,7 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility; use EP\EpProducts\Controller\ProductController; use EP\EpProducts\Controller\SearchController; +use EP\EpProducts\Controller\SearchbarController; use EP\EpProducts\Controller\SnowreportController; use EP\EpProducts\Controller\TravelinfoController; use EP\EpProducts\Controller\WatchlistController; @@ -59,10 +60,19 @@ $boot = function () { 'EpProducts', 'searchresult', [ - SearchController::class => 'searchresult,processSearch', + SearchController::class => 'searchresult', ], [ - SearchController::class => 'searchresult,processSearch', + SearchController::class => 'searchresult', + ] + ); + + // The search bar in the page header (lib.searchBar), not a content element + ExtensionUtility::configurePlugin( + 'EpProducts', + 'searchbar', + [ + SearchbarController::class => 'index', ] ); @@ -123,6 +133,12 @@ $boot = function () { // The client-side callers that vary month/year (calendar_controller.js) send them in a // POST body, which the cHash never inspects, so nothing depends on the exclusion. $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'f'; + // The search bar sends its fields by GET to the search page, whose results are not cached + // (SearchController::SEARCHBAR_PARAMETERS) + array_push( + $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'], + ...SearchController::SEARCHBAR_PARAMETERS + ); $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['PassthroughMapper'] = PassthroughMapper::class; diff --git a/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript b/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript new file mode 100644 index 0000000..1e69b05 --- /dev/null +++ b/packages/ep_theme/Configuration/TypoScript/Library/SearchBar.typoscript @@ -0,0 +1,14 @@ +# The search bar in the page header, open (sticky once scrolled past) or collapsed behind the +# search icon of the main navigation (product and hotel pages) +lib.searchBar = USER +lib.searchBar { + userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run + extensionName = EpProducts + pluginName = searchbar + settings =< plugin.tx_epproducts.settings + persistence =< plugin.tx_epproducts.persistence + view =< plugin.tx_epproducts.view +} + +lib.searchBarCollapsed < lib.searchBar +lib.searchBarCollapsed.settings.collapsed = 1 diff --git a/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript b/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript new file mode 100644 index 0000000..d9cf1a3 --- /dev/null +++ b/packages/ep_theme/Configuration/TypoScript/Page/Searchbar.typoscript @@ -0,0 +1,25 @@ +# The search bar without the page around it, on the site root (/searchbar, /searchbar-collapsed). +# The static pages of the hybrid setup load it with htmx where the export had the bar: it is the +# same on every page of a site, and its options follow the imported products (ep:import-dates +# flushes the page cache). The form sends its fields by GET to the search page. + +tx_eptheme_searchbar = PAGE +tx_eptheme_searchbar { + typeNum = 1706 + config { + disableAllHeaderCode = 1 + disablePrefixComment = 1 + additionalHeaders.10.header = Content-Type: text/html; charset=utf-8 + additionalHeaders.20.header = X-Robots-Tag: noindex, nofollow + admPanel = 0 + debug = 0 + } + + 10 =< lib.searchBar +} + +tx_eptheme_searchbar_collapsed < tx_eptheme_searchbar +tx_eptheme_searchbar_collapsed { + typeNum = 1707 + 10 =< lib.searchBarCollapsed +} diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html index 64be84e..6818ea7 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Detail.html @@ -2,8 +2,11 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"> - - + + + + +
diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html index 6317c28..215d855 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/Subpage.html @@ -5,6 +5,11 @@
+ + + + +
diff --git a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html index 41b820f..9336a40 100644 --- a/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html +++ b/packages/ep_theme/Resources/Private/Layouts/Page/SubpageSidebarRight.html @@ -5,6 +5,11 @@
+ + + + +
diff --git a/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html b/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html new file mode 100644 index 0000000..9139ae7 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Searchbar/Index.html @@ -0,0 +1,280 @@ + + + + + +
+ data-searchbar-collapsed-value="true" + + data-action="datepicker:range-select->searchbar#setDateRange searchbar-toggle:toggle@window->searchbar#toggle"> + + GET to the search page, read by SearchController::initializeSearchresultAction() + (SEARCHBAR_PARAMETERS). searchbar_controller.js finds the fields by their target. + +
+ + + + + + +
+ + data-controller="sticky" + data-sticky-offset-value="650" + data-sticky-padding-value="7.5rem" + data-sticky-disabled-value="false" + data-sticky-classes-value="sticky reduced" + + + data-searchbar-target="container"> +
+
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+ +
+
+ + +
+
+
+
+
+
+ +