feat: search bar on TYPO3 13, loaded into the static pages, searching by GET
The search bar in the page header is back (SearchbarController, lib.searchBar and lib.searchBarCollapsed), in the Subpage, SubpageSidebarRight and Detail layouts as on TYPO3 10; product pages get the collapsed bar and its toggle button in the main navigation again. - The bar takes no arguments, so it is the same on every page of a site and cacheable. Page types 1706/1707 (/searchbar, /searchbar-collapsed on the ep-reisen root, also under /en/) render it alone for the static pages, which replace their exported bar with it via htmx. The suffixes have no leading slash: on the root page the route path has none, so "/searchbar" would never match (as /form and /snowreport do not there either). - The form sends dateFrom, dateTo, pax, destinationType, destinationUid and priceRange by GET to the search page instead of POSTing to processSearch: no signed fields and no redirect, and URLs of about 100 characters instead of about 870. SearchController reads them (SEARCHBAR_PARAMETERS, excluded from the cHash; the results are not cached) and drops anything but a real Y-m-d date, which made DateUtility::formatDateRange() throw. processSearch is removed. - The date range label is empty without arguments, so the template no longer needs ep:dateRange. Compared with TYPO3 10: the bar's markup matches apart from the form (open, collapsed, /en/); seven searches by GET load the same filter settings and hit counts as the TYPO3 10 POST. The static pages get the island from the exporter in EP/website, see .ddev/plans/static-export-searchbar-island.patch (local only). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b5637ba86e
commit
48e049671e
11 files changed
+444
-49
No files matched your search
@@ -284,6 +284,8 @@ routeEnhancers:
|
||||
/html: 1702
|
||||
/form: 1704
|
||||
/captcha: 3413
|
||||
searchbar: 1706
|
||||
searchbar-collapsed: 1707
|
||||
/snowreport: 1705
|
||||
.json: 1803
|
||||
.ical: 1710
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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 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 <site root>/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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
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}"/>
|
||||
<f:cObject typoscriptObjectPath="lib.searchBarCollapsed"/>
|
||||
<f:alias map="{searchbarCollapsed: 1}">
|
||||
<f:render partial="Topnav" arguments="{_all}"/>
|
||||
<f:render partial="Mainnav" section="Main" arguments="{_all}"/>
|
||||
</f:alias>
|
||||
<main id="main-content">
|
||||
<f:render section="Content"/>
|
||||
<f:render section="Teasers"/>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<f:render partial="Mainnav" section="Main" arguments="{_all}"/>
|
||||
<header>
|
||||
<f:render partial="HeaderSliderSmall" arguments="{_all}"/>
|
||||
<f:if condition="{data.tx_eptheme_hide_searchbar} || {settings.hideSearchbar}">
|
||||
<f:else>
|
||||
<f:cObject typoscriptObjectPath="lib.searchBar"/>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</header>
|
||||
<main id="main-content">
|
||||
<f:cObject typoscriptObjectPath="lib.breadcrumb"/>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<f:render partial="Mainnav" section="Main" arguments="{_all}"/>
|
||||
<header>
|
||||
<f:render partial="HeaderSliderSmall" arguments="{_all}"/>
|
||||
<f:if condition="{data.tx_eptheme_hide_searchbar}">
|
||||
<f:else>
|
||||
<f:cObject typoscriptObjectPath="lib.searchBar"/>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</header>
|
||||
<main id="main-content">
|
||||
<f:cObject typoscriptObjectPath="lib.breadcrumb"/>
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||
data-namespace-typo3-fluid="true">
|
||||
|
||||
<f:layout name="Default"/>
|
||||
|
||||
<f:section name="Main">
|
||||
<div data-controller="searchbar"
|
||||
<f:if condition="{collapsed}">
|
||||
data-searchbar-collapsed-value="true"
|
||||
</f:if>
|
||||
data-action="datepicker:range-select->searchbar#setDateRange searchbar-toggle:toggle@window->searchbar#toggle">
|
||||
<f:comment>
|
||||
GET to the search page, read by SearchController::initializeSearchresultAction()
|
||||
(SEARCHBAR_PARAMETERS). searchbar_controller.js finds the fields by their target.
|
||||
</f:comment>
|
||||
<form action="{f:uri.page(pageUid: settings.defaultSearchPageUid)}" method="get" data-searchbar-target="form">
|
||||
<input type="hidden" name="dateFrom" value="{searchParams.dateFrom}" data-searchbar-target="dateFrom"/>
|
||||
<input type="hidden" name="dateTo" value="{searchParams.dateTo}" data-searchbar-target="dateTo"/>
|
||||
<input type="hidden" name="pax" value="{searchParams.pax}" data-searchbar-target="pax"/>
|
||||
<input type="hidden" name="destinationUid" value="{searchParams.destinationUid}" data-searchbar-target="destinationUid"/>
|
||||
<input type="hidden" name="destinationType" value="{searchParams.destinationType}" data-searchbar-target="destinationType"/>
|
||||
<input type="hidden" name="priceRange" value="{searchParams.priceRange}" data-searchbar-target="priceRange"/>
|
||||
<div class="searchbar{f:if(condition: collapsed, then: ' reduced hidden')}"
|
||||
<f:if condition="{collapsed}">
|
||||
<f:else>
|
||||
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"
|
||||
</f:else>
|
||||
</f:if>
|
||||
data-searchbar-target="container">
|
||||
<div class="bg-brand-gradient">
|
||||
<div class="reduced:max-w-screen-xl reduced:mx-auto reduced:px-4 flex flex-col reduced:flex-row reduced:items-center">
|
||||
<div class="flex items-start justify-between reduced:flex-1 pt-8 px-8 reduced:px-0 reduced:py-4">
|
||||
|
||||
<div class="flex-1"
|
||||
data-controller="collapse datepicker"
|
||||
data-datepicker-inline-value="true"
|
||||
data-datepicker-date-from-value="{searchParams.dateFrom}"
|
||||
data-datepicker-date-to-value="{searchParams.dateTo}"
|
||||
data-datepicker-min-date-value="{f:format.date(date: 'now', format: 'Y-m-d')}"
|
||||
data-action="collapse:click:outside->collapse#close searchbar:input@window->collapse#close"
|
||||
data-collapse-collapsed-value="true">
|
||||
<div class="relative border-b border-r border-white/50 mr-8">
|
||||
<input type="text"
|
||||
data-action="click->collapse#toggle"
|
||||
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
|
||||
placeholder="Zeitraum"
|
||||
aria-label="Zeitraum"
|
||||
data-searchbar-target="dateRangeLabel"/>
|
||||
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 cursor-pointer"
|
||||
data-action="click->collapse#toggle">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-calendar"></use>
|
||||
</svg>
|
||||
<div class="hidden absolute z-100 top-100 mt-2 left-0 min-w-full bg-white p-4 shadow-md rounded"
|
||||
data-collapse-target="container">
|
||||
<div class="flex justify-between divide-x divide-zinc-200">
|
||||
<div class="pr-2">
|
||||
<input type="hidden"
|
||||
aria-label="Zeitraum"
|
||||
data-datepicker-target="field">
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-between pl-2">
|
||||
<f:if condition="{settings.datePickerPresets}">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<f:for each="{settings.datePickerPresets}" as="preset">
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--light button--small w-full"
|
||||
data-preset='{preset -> f:format.json() -> f:format.raw()}'
|
||||
data-action="datepicker#selectPreset">
|
||||
<span>
|
||||
{preset.label}
|
||||
</span>
|
||||
</button>
|
||||
</f:for>
|
||||
</div>
|
||||
</f:if>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--light button--small w-full"
|
||||
data-action="collapse#close">
|
||||
schließen
|
||||
</button>
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--light button--small w-full"
|
||||
data-action="datepicker#reset">
|
||||
zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-24"
|
||||
data-controller="collapse"
|
||||
data-action="collapse:click:outside->collapse#close searchbar:input@window->collapse#close"
|
||||
data-collapse-collapsed-value="true">
|
||||
<div class="relative border-b border-r border-white/50 mr-8">
|
||||
<input type="text"
|
||||
data-action="click->collapse#toggle"
|
||||
data-searchbar-target="paxLabel"
|
||||
aria-label="Personenzahl"
|
||||
value="{searchParams.pax}"
|
||||
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer"/>
|
||||
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 cursor-pointer"
|
||||
data-action="click->collapse#toggle">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-user-group"></use>
|
||||
</svg>
|
||||
<div
|
||||
class="hidden absolute z-20 top-100 mt-2 left-0 bg-white p-2 shadow-md rounded w-full h-48 overflow-y-scroll text-sm leading-6"
|
||||
data-collapse-target="container">
|
||||
<ul class="m-0 p-0">
|
||||
<f:for each="{filterOptions.pax}" as="option">
|
||||
<li>
|
||||
<button type="button"
|
||||
data-action="searchbar#setPax"
|
||||
data-option="{option}"
|
||||
class="w-full text-right">
|
||||
{option}
|
||||
</button>
|
||||
</li>
|
||||
</f:for>
|
||||
<li>
|
||||
<a href="{f:uri.typolink(parameter: settings.groupsPageUid)}"
|
||||
class="block text-right">
|
||||
> 24
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"
|
||||
data-controller="collapse"
|
||||
data-action="collapse:click:outside->collapse#close searchbar:input@window->collapse#close"
|
||||
data-collapse-collapsed-value="true">
|
||||
<div class="relative border-b border-r border-white/50 mr-8">
|
||||
<input type="text"
|
||||
data-action="click->collapse#toggle"
|
||||
data-searchbar-target="destinationLabel"
|
||||
aria-label="Reiseziel/Konzept"
|
||||
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
|
||||
placeholder="Reiseziel/Konzept"/>
|
||||
<svg
|
||||
class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 transform rotate-45 cursor-pointer"
|
||||
data-action="click->collapse#toggle">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-airplane"></use>
|
||||
</svg>
|
||||
<div
|
||||
class="hidden absolute z-20 top-100 mt-2 left-0 bg-white p-2 shadow-md rounded min-w-full h-64 overflow-y-scroll"
|
||||
data-collapse-target="container">
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<f:for each="{filterOptions.destinations}" as="destination">
|
||||
<div class="col-span-2">
|
||||
<button type="button"
|
||||
class="flex items-center space-x-2"
|
||||
data-action="searchbar#setDestination"
|
||||
data-option-uid="{destination.country.uid}"
|
||||
data-option-type="country"
|
||||
data-option-label="{destination.country.label}">
|
||||
<svg class="block w-8 h-6">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-flag-{destination.country.code -> f:format.case(mode: 'lower')}"></use>
|
||||
</svg>
|
||||
<span>{destination.country.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-span-3 flex flex-col items-start space-y-1">
|
||||
<f:for each="{destination.regions}" as="region">
|
||||
<button type="button"
|
||||
data-action="searchbar#setDestination"
|
||||
data-option-uid="{region.uid}"
|
||||
data-option-type="region"
|
||||
data-option-label="{region.label}">
|
||||
{region.label}
|
||||
</button>
|
||||
</f:for>
|
||||
</div>
|
||||
</f:for>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 gap-4 border-t py-2 my-2">
|
||||
<div class="col-span-2">
|
||||
<strong>
|
||||
Konzept
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-span-3 flex flex-col items-start space-y-1">
|
||||
<f:for each="{filterOptions.concepts}" as="concept">
|
||||
<button type="button"
|
||||
data-action="searchbar#setDestination"
|
||||
data-option-uid="{concept.uid}"
|
||||
data-option-type="concept"
|
||||
data-option-label="{concept.name}">
|
||||
{concept.name}
|
||||
</button>
|
||||
</f:for>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="button bg-button button--small"
|
||||
data-action="searchbar#resetDestination">
|
||||
zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-32"
|
||||
data-controller="collapse"
|
||||
data-action="collapse:click:outside->collapse#close searchbar:input@window->collapse#close"
|
||||
data-collapse-collapsed-value="true">
|
||||
<div class="relative border-r border-b border-white/50">
|
||||
<input type="text"
|
||||
aria-label="Preis"
|
||||
data-action="click->collapse#toggle"
|
||||
data-searchbar-target="priceRangeLabel"
|
||||
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
|
||||
placeholder="Preis"/>
|
||||
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 cursor-pointer"
|
||||
data-action="click->collapse#toggle">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-euro"></use>
|
||||
</svg>
|
||||
<div
|
||||
class="hidden absolute z-20 top-100 left-0 bg-white p-2 shadow-md w-full h-48 overflow-y-scroll text-sm leading-7"
|
||||
data-collapse-target="container">
|
||||
<ul class="m-0 p-0">
|
||||
<li>
|
||||
<button type="button"
|
||||
data-action="searchbar#setPriceRange"
|
||||
data-option-value=""
|
||||
data-label-value="beliebig"
|
||||
class="w-full text-right">
|
||||
beliebig
|
||||
</button>
|
||||
</li>
|
||||
<f:for each="{filterOptions.priceRanges}" as="range" iteration="iteration">
|
||||
<li>
|
||||
<button type="button"
|
||||
data-action="searchbar#setPriceRange"
|
||||
data-option-value="{range.uid}"
|
||||
data-label-value="{range.formatted}"
|
||||
class="w-full text-right">
|
||||
{range.formatted}
|
||||
</button>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="hidden reduced:block reduced:ml-4" title="Suchen">
|
||||
<svg class="w-8 h-8 text-white bg-badge-gradient">
|
||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-chevron-right"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-end mt-6 reduced:hidden">
|
||||
<button type="submit"
|
||||
class="w-1/3 text-white uppercase py-2">
|
||||
Detailsuche
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="w-1/3 text-white bg-badge-gradient hover:bg-none hover:bg-ep-pink transition-colors uppercase py-2">
|
||||
Alle Reisen anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user