Migrate mobile search

This commit is contained in:
Björn Fromme
2021-10-25 14:41:27 +02:00
parent d8b50379b5
commit 8228c0d153
11 changed files with 198 additions and 70 deletions
@@ -75,6 +75,7 @@ class AjaxFilterPanelController extends ActionController
$this->view->assignMultiple([
'filterSettings' => $filterSettings,
'filterOptions' => $filterOptions,
'stimulusController' => 'mobile-search',
]);
}
@@ -99,12 +99,7 @@ class AjaxSearchController extends ActionController
$organic = false;
}
$filterOptions = $this->filterService->getFilterOptions(
$filterSettings,
$excludedConceptUids,
$excludedRegionUids,
$forcedConceptUids
);
$filterOptions = $this->filterService->collectFilterOptionsFromResult($searchResult);
$referringPageUri = $this
->uriBuilder
@@ -124,6 +119,7 @@ class AjaxSearchController extends ActionController
'filterOptions' => $filterOptions,
'searchResult' => $searchResult->getData(),
'referringPageUri' => $referringPageUri,
'stimulusController' => 'search',
'meta' => [
'organic' => $organic,
'total' => $searchResult->getTotal(),
@@ -140,12 +140,7 @@ class SearchController extends ActionController
$organic = false;
}
$filterOptions = $this->filterService->getFilterOptions(
$filterSettings,
$excludedConceptUids,
$excludedRegionUids,
$forcedConceptUids
);
$filterOptions = $this->filterService->collectFilterOptionsFromResult($searchResult);
$referringPageUri = $this
->uriBuilder
@@ -165,6 +160,7 @@ class SearchController extends ActionController
'filterOptions' => $filterOptions,
'searchResult' => $searchResult->getData(),
'referringPageUri' => $referringPageUri,
'stimulusController' => 'search',
'meta' => [
'organic' => $organic,
'total' => $searchResult->getTotal(),
@@ -96,9 +96,17 @@ class FilterOptions implements \JsonSerializable
*/
protected $busAvailable;
/**
* @var int
*/
protected $resultCount;
public function __construct()
{
$this->pax = range(1, 20);
$this->pax = [
0 => 'beliebig',
...array_combine(range(1, 20), range(1, 20))
];
}
/**
@@ -277,6 +285,22 @@ class FilterOptions implements \JsonSerializable
$this->busAvailable = $busAvailable;
}
/**
* @return int
*/
public function getResultCount(): ?int
{
return $this->resultCount;
}
/**
* @param int $resultCount
*/
public function setResultCount(int $resultCount)
{
$this->resultCount = $resultCount;
}
/**
* @return array
*/
@@ -294,6 +318,7 @@ class FilterOptions implements \JsonSerializable
'dateFrom' => ($this->getDateFrom() !== null) ? $this->getDateFrom()->format('Y-m-d') : null,
'dateTo' => ($this->getDateTo() !== null) ? $this->getDateTo()->format('Y-m-d') : null,
'busAvailable' => $this->getBusAvailable(),
'resultCount' => $this->getResultCount(),
];
}
@@ -89,17 +89,22 @@ class ProductRepository extends AbstractRepository
$query = $qb
->select(
'date.concept as conceptUid', 'date.concept_name as conceptName', 'date.concept_code as conceptCode',
'date.concept_sorting as conceptSorting',
'date.product as productUid', 'date.product_name as productName',
'date.product_detail_page as productDetailPage', 'date.product_feature as productFeature',
'date.daytrip as daytrip', 'date.country as countryUid','date.country_name as countryName',
'date.country_code as countryCode', 'date.region as regionUid', 'date.region_name as regionName',
'date.region_feature as regionFeature', 'date.date_start as dateStart', 'date.date_end as dateEnd',
'date.hotel as hotelUid', 'date.hotel_name as hotelName', 'date.hotel_category as hotelCategory',
'date.hotel_type as hotelType',
'date.earlybird as earlybird', 'date.new as new', 'date.low_contingent as lowContingent',
'date.board as board', 'date.product_fact as productFact', 'date.hotel_fact as hotelFact',
'date.region_fact as regionFact', 'date.services_included as servicesIncluded',
'date.board as board', 'date.board_bus_pro_id as boardUid',
'date.product_fact as productFact', 'date.hotel_fact as hotelFact', 'date.region_fact as regionFact',
'date.services_included as servicesIncluded',
'date.bus_available as busAvailable',
'date.min_price as minPrice', 'date.nights as nights', 'date.available as available',
'date.pseudo_price as pseudoPrice', 'date.hide_booking_button as hideBookingButton'
'date.pseudo_price as pseudoPrice', 'date.hide_booking_button as hideBookingButton',
'room.type as roomType'
)
->from('tx_epproducts_domain_model_date', 'date')
->leftJoin('date', 'tx_epproducts_domain_model_room', 'room', 'room.date = date.uid')
@@ -28,6 +28,7 @@ namespace EP\EpProducts\Service;
***************************************************************/
use EP\EpProducts\Domain\Model\FilterOptions;
use EP\EpProducts\Domain\Model\SearchResult;
use EP\EpProducts\Domain\Repository\DateRepository;
use Symfony\Component\OptionsResolver\OptionsResolver;
use TYPO3\CMS\Core\SingletonInterface;
@@ -116,10 +117,10 @@ class FilterService implements SingletonInterface
$filterSettings['priceRanges'] = [ $searchParams['priceRange'] ];
}
if ($searchParams['pax']) {
$filterSettings['pax'] = $searchParams['pax'];
$filterSettings['pax'] = (int)$searchParams['pax'];
}
if ($searchParams['nights']) {
$filterSettings['nights'] = $searchParams['nights'];
$filterSettings['nights'] = (int)$searchParams['nights'];
}
return $filterSettings;
@@ -157,7 +158,7 @@ class FilterService implements SingletonInterface
$filterSettings['dateTo'] = $staticSearchParams['date_to'];
}
if (!empty($staticSearchParams['pax']) && (int)$staticSearchParams['pax'] > 0) {
$filterSettings['pax'] = $staticSearchParams['pax'];
$filterSettings['pax'] = (int)$staticSearchParams['pax'];
}
return $filterSettings;
@@ -183,6 +184,124 @@ class FilterService implements SingletonInterface
return $this->preprocessFilterOptions($filterOptionsData);
}
public function collectFilterOptionsFromResult(SearchResult $searchResult): FilterOptions
{
$priceRanges = [];
$busAvailable = false;
$nights = [];
$concepts = [];
$destinations = [];
$hotelTypes = [];
$roomTypes = [];
$boardTypes = [];
$dateMin = $dateMax = null;
foreach ($searchResult->getData() as $row) {
$concept = $row['concept'];
if (!array_key_exists($concept['uid'], $concepts)) {
$concepts[$concept['sorting']] = [
'uid' => $concept['uid'],
'name' => $concept['name'],
'code' => $concept['code'],
];
}
foreach ($row['items'] as $item) {
if (null === $dateMin || $item['minDateStart'] < $dateMin) {
$dateMin = $item['minDateStart'];
}
if (null === $dateMax || $item['maxDateEnd'] > $dateMax) {
$dateMax = $row['itemMaxDateEnd'];
}
if ((bool) $row['busAvailable'] === true) {
$busAvailable = true;
}
foreach ($item['nights'] as $nightsOption) {
$duration = (int) $nightsOption <= 4 ? self::DURATION_SHORT : self::DURATION_LONG;
if (!\in_array($duration, $nights, false)) {
$nights[] = $duration;
}
}
if (!array_key_exists($item['country']['uid'], $destinations)) {
$destinations[$item['country']['uid']] = [
'country' => [
'uid' => $item['country']['uid'],
'label' => $item['country']['name'],
'code' => $item['country']['code'],
],
'regions' => [],
];
}
if (!array_key_exists($item['region']['uid'], $destinations[$item['country']['uid']]['regions'])) {
$destinations[$item['country']['uid']]['regions'][$item['region']['uid']] = [
'uid' => $item['region']['uid'],
'label' => $item['region']['name'],
];
}
if (!\in_array($item['hotel']['type'], $hotelTypes, false)) {
$hotelTypes[] = $item['hotel']['type'];
}
foreach ($item['rooms'] as $roomType) {
if (!\in_array($roomType, $roomTypes, false)) {
$roomTypes[] = $roomType;
}
}
$boardType = [
'uid' => $item['boardUid'],
'label' => $item['board'],
];
if (!\in_array($boardType, $boardTypes, false)) {
$boardTypes[] = $boardType;
}
foreach (FilterOptions::$priceRangeOptions as $index => $priceRange) {
if (array_key_exists($index, $priceRanges)) {
continue;
}
[$min, $max] = $priceRange;
if ($item['minPrice'] >= $min && $item['minPrice'] < $max) {
$range = FilterOptions::$priceRangeOptions[$index];
$priceRanges[$index] = [
'uid' => $index,
'min' => $range[0],
'max' => $range[1],
'formatted' => $this->formatPriceRange($range[0], $range[1]),
];
}
}
}
}
foreach ($destinations as $key => $value) {
usort($value['regions'], function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
$destinations[$key] = $value;
}
sort($destinations);
sort($nights);
sort($roomTypes);
ksort($priceRanges);
usort($boardTypes, function ($a, $b) {
return strcmp($a['label'], $b['label']);
});
$filterOptions = new FilterOptions();
$filterOptions->setPriceRanges($priceRanges);
$filterOptions->setDateFrom($dateMin);
$filterOptions->setDateTo($dateMax);
$filterOptions->setConcepts($concepts);
$filterOptions->setDestinations($destinations);
$filterOptions->setHotelTypes($hotelTypes);
$filterOptions->setRoomTypes($roomTypes);
$filterOptions->setBoardTypes($boardTypes);
$filterOptions->setBusAvailable($busAvailable);
$filterOptions->setNights($nights);
$filterOptions->setResultCount($searchResult->getTotal());
return $filterOptions;
}
public function preprocessFilterOptions(array $filterOptionsData): FilterOptions
{
$priceRanges = [];
@@ -256,15 +375,14 @@ class FilterService implements SingletonInterface
// Board types
$boardType = [
'uid' => $row['boardUid'],
'label' => $row['boardType'],
'label' => $row['board'],
];
if (!\in_array($boardType, $boardTypes, false)) {
$boardTypes[] = $boardType;
}
// Price range
foreach (FilterOptions::$priceRangeOptions as $index => $priceRange)
{
foreach (FilterOptions::$priceRangeOptions as $index => $priceRange) {
if (array_key_exists($index, $priceRanges)) {
continue;
}
@@ -108,6 +108,7 @@ class SearchResultService implements SingletonInterface
'uid' => $row['conceptUid'],
'name' => $row['conceptName'],
'code' => $row['conceptCode'],
'sorting' => $row['conceptSorting'],
],
'items' => [],
];
@@ -130,6 +131,7 @@ class SearchResultService implements SingletonInterface
'earlybird' => $row['earlybird'],
'daytrip' => $row['daytrip'],
'hideBookingButton' => $row['hideBookingButton'],
'busAvailable' => $row['busAvailable'],
'lowContingent' => $row['lowContingent'],
'servicesIncluded' => unserialize($row['servicesIncluded'], ['allowed_classes' => false]),
'hotel' => [
@@ -139,13 +141,16 @@ class SearchResultService implements SingletonInterface
'images' => $this->hotelImageService->getImages($row['hotelUid']),
'fact' => $row['hotelFact'],
'category' => $hotelCategory,
'type' => $row['hotelType'],
],
'country' => [
'uid' => $row['countryUid'],
'code' => $row['countryCode'],
'name' => $row['countryName'],
'flag' => $this->flagIconService->getImage($row['countryCode']),
],
'region' => [
'uid' => $row['regionUid'],
'name' => $row['regionName'],
'fact' => $row['regionFact'],
'feature' => $row['regionFeature'],
@@ -159,14 +164,20 @@ class SearchResultService implements SingletonInterface
'fact' => $row['productFact'],
],
'board' => $row['board'],
'boardUid' => $row['boardUid'],
'dates' => [],
'nights' => [],
'rooms' => [],
];
$total++;
}
$item =& $items[$key];
if (!\in_array($row['roomType'], $item['rooms'], false)) {
$item['rooms'][] = $row['roomType'];
}
if (!\in_array($row['nights'], $item['nights'], false)) {
$item['nights'][] = $row['nights'];
}
@@ -13,8 +13,10 @@ export default class extends Controller {
static values = {
optionsUri: String,
initialOptionsUri: String,
searchUri: String,
loading: Boolean,
loaded: Boolean,
show: Boolean,
}
@@ -25,13 +27,14 @@ export default class extends Controller {
open(e) {
if (e.detail === 'mobile-search') {
this.showValue = true
this.load()
if (false === this.loadedValue) {
this.load()
}
}
}
close() {
this.showValue = false
this.containerTarget.innerHTML = ''
}
filter() {
@@ -46,12 +49,14 @@ export default class extends Controller {
}
load(options) {
let uri = this.loadedValue ? this.optionsUriValue : this.initialOptionsUriValue
this.loadingValue = true
fetch(this.optionsUriValue, options)
fetch(uri, options)
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.containerTarget.innerHTML = html
this.loadedValue = true
})
.finally(() => {
this.loadingValue = false
@@ -73,6 +78,7 @@ export default class extends Controller {
}
selectDateRange(e) {
e.stopPropagation()
this.dateFromTarget.value = e.detail.from
this.dateToTarget.value = e.detail.to
this.filter()
@@ -34,8 +34,7 @@
data-action="click->collapse#toggle"
class="w-full px-0 bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase cursor-pointer placeholder-white"
placeholder="Zeitraum"
value="{ep:dateRange(dateFrom: filterSettings.dateFrom, dateTo: filterSettings.dateTo, includeLabel: 0)}"
data-searchbar-target="dateRangeLabel"/>
value="{ep:dateRange(dateFrom: filterSettings.dateFrom, dateTo: filterSettings.dateTo, includeLabel: 0)}"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mt-2 cursor-pointer"
data-action="click->collapse#toggle">
<use href="#icon-calendar"></use>
@@ -106,7 +105,7 @@
<label>
<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"
property="conceptUids"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{concept.uid}"/>
<span class="inline-block py-1 px-2 bg-concept-{concept.code} text-white">
{concept.name}
@@ -141,7 +140,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="countryUids"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{destination.country.uid}"/>
<span class="pl-2 leading-none">
<strong>{destination.country.label}</strong>
@@ -157,7 +156,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="regionUids"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{region.uid}"/>
<span class="leading-none pl-2">
{region.label}
@@ -194,7 +193,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="priceRanges"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{range.uid}"/>
<span class="leading-none pl-2">
{ep:priceRange(min: range.min, max: range.max)}
@@ -224,7 +223,7 @@
<f:form.select class="w-full border-0 focus:ring-0 bg-right mt-2 p-0"
property="pax"
options="{filterOptions.pax}"
data="{action: 'search#filter mobile-search#filter'}"/>
data="{action: '{stimulusController}#filter'}"/>
</div>
</div>
@@ -256,7 +255,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="hotelTypes"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{type}"/>
<span class="leading-none pl-2">
{hotelTypes.{type}}
@@ -290,7 +289,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="boardTypes"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{type.uid}"/>
<span class="pl-2 leading-none">
{type.label}
@@ -332,7 +331,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="roomTypes"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="{type}"/>
<span class="leading-none pl-2">
{roomTypes.{type}}
@@ -365,7 +364,7 @@
<label class="flex items-start">
<f:form.checkbox class="w-5 h-5 border-ep-primary ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
property="bus"
data="{action: 'search#filter mobile-search#filter'}"
data="{action: '{stimulusController}#filter'}"
value="1"/>
<span class="leading-none pl-2">
Busanreise
@@ -22,42 +22,11 @@
</button>
</div>
</div>
<div class="fixed lg:hidden top-0 left-0 inset-0 w-screen h-screen pt-14 pb-10 z-50 bg-gray-700 hidden"
data-controller="mobile-search"
data-mobile-search-options-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'options', pageUid: settings.defaultAjaxUid, arguments: '{filterSettings: filterSettings}', format: 'html')}"
data-action="overlay-toggle:toggle@window->mobile-search#open">
<div class="fixed top-0 left-0 z-100 inset-x-0 px-4 py-2 flex justify-between bg-white shadow-md">
<a href="{f:uri.typolink(parameter: settings.defaultHomeUid)}"
class="block lg:pb-4"
title="{settings.defaultTitle}">
<img class="block h-10 lg:h-12 w-auto" src="{f:uri.image(src: settings.logoImage)}" alt="{settings.defaultTitle}">
</a>
<button type="button"
class="focus:outline-none" data-action="mobile-search#close">
<svg class="w-8 h-8">
<use href="#icon-close"></use>
</svg>
</button>
</div>
<div data-mobile-search-target="container"
class="h-full w-full overflow-y-scroll p-4 bg-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 transform -translate-xy-1/2 flex items-center space-x-4 bg-ep-secondary text-white text-lg p-4 rounded">
<svg class="w-6 h-6">
<use href="#icon-spinner"></use>
</svg>
<span>Suche wird geladen...</span>
</div>
</div>
</div>
</div>
<div class="col-span-3 lg:col-span-2 relative">
<f:render partial="Search/Result" arguments="{_all}"/>
</div>
<f:render partial="Search/MobileSearch" arguments="{_all}"/>
</div>
</html>
@@ -1,12 +1,14 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
<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="fixed lg:hidden top-0 left-0 inset-0 w-screen h-screen pt-14 pb-10 z-50 bg-gray-700 hidden"
data-controller="mobile-search"
data-mobile-search-options-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'options', pageUid: settings.defaultAjaxUid, format: 'html')}"
data-mobile-search-initial-options-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'options', pageUid: settings.defaultAjaxUid, arguments: '{filterSettings: filterSettings}', format: 'html')}"
data-mobile-search-search-uri-value="{ep:uri.ajax(controller: 'AjaxFilterPanel', action: 'search', pageUid: settings.defaultAjaxUid, format: 'json')}"
data-action="overlay-toggle:toggle@window->mobile-search#open">
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 flex justify-between bg-white shadow-md">
<a href="{f:uri.typolink(parameter: settings.defaultHomeUid)}"
class="block lg:pb-4"