WIP Migrate to stimulus.js

This commit is contained in:
Björn Fromme
2021-08-15 19:10:32 +02:00
parent eed8af3bd4
commit f2b6f89633
72 changed files with 1680 additions and 1284 deletions
+124 -1
View File
@@ -17,6 +17,7 @@
"bourbon": "^4.3.2",
"cookieconsent": "^3.1.0",
"dayjs": "^1.10.6",
"es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.9",
"font-awesome": "^4.7.0",
"glightbox": "^3.0.9",
@@ -28,11 +29,14 @@
"picturefill": "^3.0.3",
"slick-carousel": "^1.8.1",
"sticky-js": "^1.3.0",
"stimulus": "^2.0.0",
"stimulus-use": "^0.25.4",
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vue-scrollto": "^2.20.0",
"vue-session": "^1.0.0",
"vuex": "^3.6.2"
"vuex": "^3.6.2",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
@@ -1650,6 +1654,32 @@
"resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-2.4.21.tgz",
"integrity": "sha512-LdIwSnmNsGybCCnA3QG2dOeRv/rQl46jf7FflXxMGX0ADYWyL6UJHWJbMZvHBi5I67uRJY1ga/W+CvJ39nH26A=="
},
"node_modules/@stimulus/core": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/core/-/core-2.0.0.tgz",
"integrity": "sha512-ff70GafKtzc8zQ1/cG+UvL06GcifPWovf2wBEdjLMh9xO2GOYURO3y2RYgzIGYUIBefQwyfX2CLfJdZFJrEPTw==",
"dependencies": {
"@stimulus/mutation-observers": "^2.0.0"
}
},
"node_modules/@stimulus/multimap": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/multimap/-/multimap-2.0.0.tgz",
"integrity": "sha512-pMBCewkZCFVB3e5mEMoyO9+9aKzHDITmf3OnPun51YWxlcPdHcwbjqm1ylK63fsoduIE+RowBpFwFqd3poEz4w=="
},
"node_modules/@stimulus/mutation-observers": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/mutation-observers/-/mutation-observers-2.0.0.tgz",
"integrity": "sha512-kx4VAJdPhIGBQKGIoUDC2tupEKorG3A+ckc2b1UiwInKTMAC1axOHU8ebcwhaJIxRqIrs8//4SJo9YAAOx6FEg==",
"dependencies": {
"@stimulus/multimap": "^2.0.0"
}
},
"node_modules/@stimulus/webpack-helpers": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/webpack-helpers/-/webpack-helpers-2.0.0.tgz",
"integrity": "sha512-D6tJWsAC024MwGEIKlUVYU8Ln87mlrmiwHvYAjipg+s8H4eLxUMQ3PZkWyPevfipH+oR3leuHsjYsK1gN5ViQA=="
},
"node_modules/@symfony/webpack-encore": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@symfony/webpack-encore/-/webpack-encore-1.5.0.tgz",
@@ -4305,6 +4335,11 @@
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-slide-up-down": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es6-slide-up-down/-/es6-slide-up-down-1.0.0.tgz",
"integrity": "sha1-RTBJFkFvRliBBk6Q7t8SHTZmT14="
},
"node_modules/es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@@ -5242,6 +5277,11 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"node_modules/hotkeys-js": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
},
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -9394,6 +9434,26 @@
"resolved": "https://registry.npmjs.org/sticky-js/-/sticky-js-1.3.0.tgz",
"integrity": "sha512-X5zWjFrw7c2xE4tUtSeK7I+1o252JOyZOlzxsu1Q9qZ6hlk2NCd9Sr/AFxf8hfGkLnE1Pak+iLaDyd883LFRgw=="
},
"node_modules/stimulus": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/stimulus/-/stimulus-2.0.0.tgz",
"integrity": "sha512-xipy7BS5TVpg4fX6S8LhrYZp7cmHGjmk09WSAiVx1gF5S5g43IWsuetfUhIk8HfHUG+4MQ9nY0FQz4dRFLs/8w==",
"dependencies": {
"@stimulus/core": "^2.0.0",
"@stimulus/webpack-helpers": "^2.0.0"
}
},
"node_modules/stimulus-use": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/stimulus-use/-/stimulus-use-0.25.4.tgz",
"integrity": "sha512-Rr4m4PpcOni7y4KiwUMdCuUJ5IPE2LEELwoz8e+VB3jbfPb9DNjrurCTKCWT5BiPBrn9OEfhnWJIRPcbxUpQvQ==",
"dependencies": {
"hotkeys-js": ">=3"
},
"peerDependencies": {
"stimulus": ">=1.1.1 <3"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -10835,6 +10895,11 @@
"node": ">=0.8.0"
}
},
"node_modules/whatwg-fetch": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
"integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA=="
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -12236,6 +12301,32 @@
"resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-2.4.21.tgz",
"integrity": "sha512-LdIwSnmNsGybCCnA3QG2dOeRv/rQl46jf7FflXxMGX0ADYWyL6UJHWJbMZvHBi5I67uRJY1ga/W+CvJ39nH26A=="
},
"@stimulus/core": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/core/-/core-2.0.0.tgz",
"integrity": "sha512-ff70GafKtzc8zQ1/cG+UvL06GcifPWovf2wBEdjLMh9xO2GOYURO3y2RYgzIGYUIBefQwyfX2CLfJdZFJrEPTw==",
"requires": {
"@stimulus/mutation-observers": "^2.0.0"
}
},
"@stimulus/multimap": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/multimap/-/multimap-2.0.0.tgz",
"integrity": "sha512-pMBCewkZCFVB3e5mEMoyO9+9aKzHDITmf3OnPun51YWxlcPdHcwbjqm1ylK63fsoduIE+RowBpFwFqd3poEz4w=="
},
"@stimulus/mutation-observers": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/mutation-observers/-/mutation-observers-2.0.0.tgz",
"integrity": "sha512-kx4VAJdPhIGBQKGIoUDC2tupEKorG3A+ckc2b1UiwInKTMAC1axOHU8ebcwhaJIxRqIrs8//4SJo9YAAOx6FEg==",
"requires": {
"@stimulus/multimap": "^2.0.0"
}
},
"@stimulus/webpack-helpers": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@stimulus/webpack-helpers/-/webpack-helpers-2.0.0.tgz",
"integrity": "sha512-D6tJWsAC024MwGEIKlUVYU8Ln87mlrmiwHvYAjipg+s8H4eLxUMQ3PZkWyPevfipH+oR3leuHsjYsK1gN5ViQA=="
},
"@symfony/webpack-encore": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@symfony/webpack-encore/-/webpack-encore-1.5.0.tgz",
@@ -14384,6 +14475,11 @@
"es6-symbol": "^3.1.1"
}
},
"es6-slide-up-down": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es6-slide-up-down/-/es6-slide-up-down-1.0.0.tgz",
"integrity": "sha1-RTBJFkFvRliBBk6Q7t8SHTZmT14="
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@@ -15116,6 +15212,11 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"hotkeys-js": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
},
"hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -18251,6 +18352,23 @@
"resolved": "https://registry.npmjs.org/sticky-js/-/sticky-js-1.3.0.tgz",
"integrity": "sha512-X5zWjFrw7c2xE4tUtSeK7I+1o252JOyZOlzxsu1Q9qZ6hlk2NCd9Sr/AFxf8hfGkLnE1Pak+iLaDyd883LFRgw=="
},
"stimulus": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/stimulus/-/stimulus-2.0.0.tgz",
"integrity": "sha512-xipy7BS5TVpg4fX6S8LhrYZp7cmHGjmk09WSAiVx1gF5S5g43IWsuetfUhIk8HfHUG+4MQ9nY0FQz4dRFLs/8w==",
"requires": {
"@stimulus/core": "^2.0.0",
"@stimulus/webpack-helpers": "^2.0.0"
}
},
"stimulus-use": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/stimulus-use/-/stimulus-use-0.25.4.tgz",
"integrity": "sha512-Rr4m4PpcOni7y4KiwUMdCuUJ5IPE2LEELwoz8e+VB3jbfPb9DNjrurCTKCWT5BiPBrn9OEfhnWJIRPcbxUpQvQ==",
"requires": {
"hotkeys-js": ">=3"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -19317,6 +19435,11 @@
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"whatwg-fetch": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
"integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+6 -2
View File
@@ -17,6 +17,7 @@
"bourbon": "^4.3.2",
"cookieconsent": "^3.1.0",
"dayjs": "^1.10.6",
"es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.9",
"font-awesome": "^4.7.0",
"glightbox": "^3.0.9",
@@ -28,11 +29,14 @@
"picturefill": "^3.0.3",
"slick-carousel": "^1.8.1",
"sticky-js": "^1.3.0",
"stimulus": "^2.0.0",
"stimulus-use": "^0.25.4",
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vue-scrollto": "^2.20.0",
"vue-session": "^1.0.0",
"vuex": "^3.6.2"
"vuex": "^3.6.2",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
@@ -54,8 +58,8 @@
"tailwindcss": "^2.2.7",
"tailwindcss-alt": "^3.0.0",
"tinycolor2": "^1.4.1",
"vue-loader": "^15.9.7",
"vue-custom-element": "^3.3.0",
"vue-loader": "^15.9.7",
"vue-template-compiler": "^2.6.14"
}
}
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\Domain\Model\Product;
use EP\EpProducts\Domain\Repository\ProductRepository;
@@ -85,12 +84,6 @@ class AjaxDateController extends ActionController
$this->productRepository = $productRepository;
}
public function initializeAction()
{
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$this->processFilterSettingsArgument($forcedConceptUids);
}
public function initializeDatesAction()
{
if ($this->request->hasArgument('dateFrom')) {
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Service\FilterService;
use EP\EpProducts\Service\SearchResultUrlService;
use EP\EpProducts\Service\SearchResultService;
@@ -66,8 +65,7 @@ class AjaxSearchController extends ActionController
FilterService $filterService,
SearchResultService $searchResultService,
SearchResultUrlService $searchResultUrlService
)
{
) {
parent::__construct();
$this->filterService = $filterService;
@@ -75,34 +73,26 @@ class AjaxSearchController extends ActionController
$this->searchResultUrlService = $searchResultUrlService;
}
public function initializeAction()
/**
* @param array $filterSettings
*/
public function searchresultAction(array $filterSettings = [])
{
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$this->processFilterSettingsArgument($forcedConceptUids);
}
/**
* @param FilterSettings $filterSettings
* @return string
* @throws \Doctrine\DBAL\DBALException
*/
public function searchresultAction(FilterSettings $filterSettings = null)
{
$dateSelected = false;
$organic = true;
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids']);
$excludedRegionUids = GeneralUtility::trimExplode(',', $this->settings['excludedRegionUids']);
if ($filterSettings === null) {
$filterSettings = $this->filterService->getDefaultFilterSettings($forcedConceptUids);
}
if ($filterSettings->getDateFrom() !== null || $filterSettings->getDateTo() !== null) {
if (isset($filterSettings['dateFrom']) || isset($filterSettings['dateTo'])) {
$dateSelected = true;
}
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
$searchResult = $this
->searchResultService
->getResult($filterSettings, $excludedConceptUids);
if ($searchResult->getTotal() === 0) {
$filterSettings = $this->filterService->getDefaultFilterSettings($forcedConceptUids);
@@ -110,11 +100,7 @@ class AjaxSearchController extends ActionController
$organic = false;
}
$options = [
'defaultDetailPageUid' => $this->settings['defaultDetailPageUid'],
];
$this->searchResultUrlService->process($searchResult, $options);
$this->searchResultUrlService->process($searchResult, $this->settings['defaultDetailPageUid']);
$filterOptions = $this->filterService->getFilterOptions(
$filterSettings,
@@ -129,7 +115,7 @@ class AjaxSearchController extends ActionController
->setTargetPageUid($this->settings['defaultSearchPageUid'])
->uriFor(
'searchresult',
['filterSettings' => $filterSettings->toArray()],
['filterSettings' => $filterSettings],
'Search',
null,
'searchresult'
@@ -144,7 +130,7 @@ class AjaxSearchController extends ActionController
'meta' => [
'organic' => $organic,
'total' => $searchResult->getTotal(),
'dateRange' => DateUtility::formatDateRange($filterSettings->getDateFrom(), $filterSettings->getDateTo()),
'dateRange' => DateUtility::formatDateRange($filterSettings['dateFrom'], $filterSettings['dateTo']),
'dateSelected' => $dateSelected,
],
]);
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\SearchParams;
use EP\EpProducts\Service\FilterService;
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
@@ -70,7 +69,7 @@ class AjaxSearchbarController extends ActionController
$searchParams = new SearchParams();
$filterSettings = $this->filterService->getDefaultFilterSettings($forcedConceptUids);
} else {
$filterSettings = FilterSettings::fromSearchParams($searchParams);
$filterSettings = $searchParams->toFilterSettings();
}
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids']);
$excludedRegionUids = GeneralUtility::trimExplode(',', $this->settings['excludedRegionUids']);
@@ -28,7 +28,6 @@ namespace EP\EpProducts\Controller;
***************************************************************/
use EP\EpProducts\Domain\Model\Concept;
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Repository\ConceptRepository;
use EP\EpProducts\Service\ProductDataService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -69,11 +68,13 @@ class ConceptController extends ActionController
}
$teaserConceptUids = GeneralUtility::trimExplode(',', $this->settings['additionalConceptUids']);
array_unshift($teaserConceptUids, $concept->getUid());
$filterSettings = new FilterSettings();
$filterSettings->setConceptUids($teaserConceptUids);
$filterSettings = [
'conceptUids' => $teaserConceptUids,
];
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$limit = $this->settings['showMoreLink'] ? 4 : 0;
$teasers = $this->productDataService->getTeasergroup($filterSettings, $excludedConceptUids, $limit);
$this->view->assignMultiple([
'teasers' => $teasers,
'concept' => $concept,
@@ -28,7 +28,6 @@ namespace EP\EpProducts\Controller;
***************************************************************/
use EP\EpProducts\Domain\Model\Country;
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Repository\CountryRepository;
use EP\EpProducts\Service\ProductDataService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -67,8 +66,9 @@ class CountryController extends ActionController
$countryUid = $this->settings['countryUid'];
$country = $this->countryRepository->findByIdentifier($countryUid);
}
$filterSettings = new FilterSettings();
$filterSettings->setCountryUids([$countryUid]);
$filterSettings = [
'countryUids' => [ $countryUid ],
];
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$limit = $this->settings['showMoreLink'] ? 4 : 0;
$teasers = $this->productDataService->getTeasergroup($filterSettings, $excludedConceptUids, $limit);
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\Domain\Repository\HotelRepository;
use EP\EpProducts\Domain\Repository\ProductRepository;
@@ -92,8 +91,9 @@ class HotelController extends ActionController
/** @var Hotel $hotel */
$hotel = $this->hotelRepository->findByIdentifier($hotelUid);
}
$filterSettings = new FilterSettings();
$filterSettings->setHotelUid($hotelUid);
$filterSettings = [
'hotelUid' => $hotelUid,
];
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$limit = $this->settings['showMoreLink'] ? 4 : 0;
$teasers = $this->productDataService->getTeasergroup($filterSettings, $excludedConceptUids, $limit);
@@ -122,7 +122,7 @@ class ProductController extends ActionController
'searchresult',
'Search',
null,
['filterSettings' => $filterSettings->toArray()],
['filterSettings' => $filterSettings],
$this->settings['defaultSearchPageUid']
);
}
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\Region;
use EP\EpProducts\Domain\Repository\RegionRepository;
use EP\EpProducts\Service\ProductDataService;
@@ -72,8 +71,9 @@ class RegionController extends ActionController
return (int)$uid;
}, $additionalRegionUids);
$teaserRegionUids = array_merge([$region->getUid()], $additionalRegionUids);
$filterSettings = new FilterSettings();
$filterSettings->setRegionUids($teaserRegionUids);
$filterSettings = [
'regionUids' => $teaserRegionUids,
];
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$limit = $this->settings['showMoreLink'] ? 4 : 0;
$teasers = $this->productDataService->getTeasergroup($filterSettings, $excludedConceptUids, $limit);
@@ -27,11 +27,13 @@ namespace EP\EpProducts\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\SearchParams;
use EP\EpProducts\Service\FilterService;
use EP\EpProducts\Service\FilterSettingsEncoder;
use EP\EpProducts\Service\SearchResultService;
use EP\EpProducts\Service\SearchResultUrlService;
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
use EP\EpProducts\Utility\DateUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
@@ -47,7 +49,17 @@ class SearchController extends ActionController
protected $filterService;
/**
* @var \EP\EpProducts\Domain\Model\FilterSettings
* @var SearchResultService
*/
private $searchResultService;
/**
* @var SearchResultUrlService
*/
private $searchResultUrlService;
/**
* @var array
*/
protected $defaultFilterSettings;
@@ -59,10 +71,16 @@ class SearchController extends ActionController
/**
* @param FilterService $filterService
*/
public function __construct(FilterService $filterService)
{
public function __construct
(
FilterService $filterService,
SearchResultService $searchResultService,
SearchResultUrlService $searchResultUrlService
) {
parent::__construct();
$this->filterService = $filterService;
$this->searchResultService = $searchResultService;
$this->searchResultUrlService = $searchResultUrlService;
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids'], true);
$excludedRegionUids = GeneralUtility::trimExplode(',', $this->settings['excludedRegionUids'], true);
@@ -104,19 +122,17 @@ class SearchController extends ActionController
/**
* @param SearchParams $searchParams
* @throws StopActionException
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException
*/
public function processSearchAction(SearchParams $searchParams)
{
$filterSettings = FilterSettings::fromSearchParams($searchParams);
$filterSettings = $searchParams->toFilterSettings();
$redirectUri = $this->uriBuilder
->reset()
->setCreateAbsoluteUri(true)
->uriFor(
'searchresult',
['filterSettings' => $filterSettings->toArray()],
['filterSettings' => $filterSettings],
null,
null,
'searchresult'
@@ -128,17 +144,33 @@ class SearchController extends ActionController
public function initializeSearchresultAction()
{
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$this->processFilterSettingsArgument($forcedConceptUids);
//$this->processFilterSettingsArgument($forcedConceptUids);
}
/**
* @param \EP\EpProducts\Domain\Model\FilterSettings $filterSettings
* @param array $filterSettings
*/
public function searchresultAction(FilterSettings $filterSettings)
public function searchresultAction(array $filterSettings = [])
{
$dateSelected = false;
$organic = true;
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids']);
$excludedRegionUids = GeneralUtility::trimExplode(',', $this->settings['excludedRegionUids']);
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
if (!empty($filterSettings['dateFrom']) || !empty($filterSettings['dateTo'])) {
$dateSelected = true;
}
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
if ($searchResult->getTotal() === 0) {
$filterSettings = $this->defaultFilterSettings;
$searchResult = $this->searchResultService->getResult($filterSettings, $excludedConceptUids);
$organic = false;
}
$this->searchResultUrlService->process($searchResult, $this->settings['defaultDetailPageUid']);
$filterOptions = $this->filterService->getFilterOptions(
$filterSettings,
@@ -147,10 +179,30 @@ class SearchController extends ActionController
$forcedConceptUids
);
$referringPageUri = $this
->uriBuilder
->reset()
->setTargetPageUid($this->settings['defaultSearchPageUid'])
->uriFor(
'searchresult',
['filterSettings' => $filterSettings],
'Search',
null,
'searchresult'
)
;
$this->view->assignMultiple([
'referringPageUid' => $this->settings['defaultSearchPageUid'],
'filterOptions' => $filterOptions,
'filterSettings' => $filterSettings,
'filterOptions' => $filterOptions,
'searchResult' => $searchResult->getData(),
'referringPageUri' => $referringPageUri,
'meta' => [
'organic' => $organic,
'total' => $searchResult->getTotal(),
'dateRange' => DateUtility::formatDateRange($filterSettings['dateFrom'], $filterSettings['dateTo']),
'dateSelected' => $dateSelected,
],
]);
}
@@ -262,7 +262,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $countryUids
*/
public function setCountryUids(array $countryUids)
public function setCountryUids(?array $countryUids)
{
$this->countryUids = $countryUids;
}
@@ -278,7 +278,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $regionUids
*/
public function setRegionUids(array $regionUids)
public function setRegionUids(?array $regionUids)
{
$this->regionUids = $regionUids;
}
@@ -294,7 +294,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $cityUids
*/
public function setCityUids($cityUids)
public function setCityUids(?array $cityUids)
{
$this->cityUids = $cityUids;
}
@@ -310,7 +310,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $conceptUids
*/
public function setConceptUids($conceptUids)
public function setConceptUids(?array $conceptUids)
{
$this->conceptUids = $conceptUids;
}
@@ -421,7 +421,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $boardTypes
*/
public function setBoardTypes($boardTypes)
public function setBoardTypes(?array $boardTypes)
{
$this->boardTypes = $boardTypes;
}
@@ -437,7 +437,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $hotelTypes
*/
public function setHotelTypes($hotelTypes)
public function setHotelTypes(?array $hotelTypes)
{
$this->hotelTypes = $hotelTypes;
}
@@ -453,7 +453,7 @@ class FilterSettings implements \JsonSerializable
/**
* @param array $roomTypes
*/
public function setRoomTypes($roomTypes)
public function setRoomTypes(?array $roomTypes)
{
$this->roomTypes = $roomTypes;
}
@@ -235,6 +235,46 @@ class SearchParams implements \JsonSerializable
$this->pageUid = $pageUid;
}
public function toFilterSettings(): array
{
$filterSettings = [];
if ($this->getDestinationType() && $this->getDestinationUid()) {
$destinationUid = $this->getDestinationUid();
switch ($this->getDestinationType()) {
case SearchParams::DESTINATION_TYPE_COUNTRY:
$filterSettings['countryUids'] = [ $destinationUid ];
break;
case SearchParams::DESTINATION_TYPE_REGION:
$filterSettings['regionUids'] = [ $destinationUid ];
break;
case SearchParams::DESTINATION_TYPE_CITY:
$filterSettings['cityUids'] = [ $destinationUid ];
break;
case SearchParams::DESTINATION_TYPE_CONCEPT:
$filterSettings['conceptUids'] = [ $destinationUid ];
break;
}
}
if (null !== $this->getDateFrom()) {
$filterSettings['dateFrom'] = $this->getDateFrom()->format('Y-m-d');
}
if (null !== $this->getDateTo()) {
$filterSettings['dateTo'] = $this->getDateTo()->format('Y-m-d');
}
if (null !== $this->getPriceRange()) {
$filterSettings['priceRanges'] = [ $this->getPriceRange() ];
}
if (null !== $this->getPax()) {
$filterSettings['pax'] = $this->getPax();
}
if (null !== $this->getNights()) {
$filterSettings['nights'] = $this->getNights();
}
return $filterSettings;
}
public function toArray(): array
{
return [
@@ -40,21 +40,14 @@ class SearchResult
*/
protected $data = [];
/**
* @var FilterSettings
*/
protected $filterSettings;
/**
* @param array $data
* @param int $total
* @param FilterSettings $filterSettings
*/
public function __construct(array $data = [], $total = 0, FilterSettings $filterSettings = null)
public function __construct(array $data = [], $total = 0)
{
$this->data = $data;
$this->total = $total;
$this->filterSettings = $filterSettings;
}
/**
@@ -89,20 +82,4 @@ class SearchResult
$this->data = $data;
}
/**
* @return FilterSettings
*/
public function getFilterSettings()
{
return $this->filterSettings;
}
/**
* @param FilterSettings $filterSettings
*/
public function setFilterSettings($filterSettings)
{
$this->filterSettings = $filterSettings;
}
}
@@ -42,7 +42,7 @@ abstract class AbstractRepository extends \TYPO3\CMS\Extbase\Persistence\Reposit
{
/** @var $querySettings \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings */
$querySettings = $this->objectManager->get(Typo3QuerySettings::class);
$querySettings->setRespectStoragePage(FALSE);
$querySettings->setRespectStoragePage(false);
$this->setDefaultQuerySettings($querySettings);
}
@@ -60,114 +60,90 @@ abstract class AbstractRepository extends \TYPO3\CMS\Extbase\Persistence\Reposit
return $query->execute();
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addDateFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addDateFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Date Start
if ($filterSettings->getDateFrom() !== null) {
if (!empty($filterSettings['dateFrom'])) {
$query
->andWhere('date.date_start >= :dateStart')
->setParameter('dateStart', $filterSettings->getDateFrom()->format('Y-m-d'))
->setParameter('dateStart', $filterSettings['dateFrom'])
;
}
// Date End
if ($filterSettings->getDateTo() !== null) {
if (!empty($filterSettings['dateTo'])) {
$query
->andWhere('date.date_end <= :dateEnd')
->setParameter('dateEnd', $filterSettings->getDateTo()->format('Y-m-d'))
->setParameter('dateEnd', $filterSettings['dateTo'])
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addHotelFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addHotelFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Hotel
if ((int) $filterSettings->getHotelUid() > 0) {
if (!empty($filterSettings['hotelUid'])) {
$query
->andWhere('date.hotel = :hotelUid')
->setParameter('hotelUid', $filterSettings->getHotelUid())
->setParameter('hotelUid', (int)$filterSettings['hotelUid'])
;
}
// Hotel types
if (count($filterSettings->getHotelTypes()) > 0) {
if (!empty($filterSettings['hotelTypes'])) {
$query
->andWhere('date.hotel_type IN (:hotelTypes)')
->setParameter('hotelTypes', $filterSettings->getHotelTypes(), Connection::PARAM_INT_ARRAY)
->setParameter('hotelTypes', (array)$filterSettings['hotelTypes'], Connection::PARAM_INT_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addCountryRegionAndCityFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addCountryRegionAndCityFilterConditions(QueryBuilder $query, array $filterSettings): void
{
if (count($filterSettings->getCityUids()) > 0) {
if (!empty($filterSettings['cityUids'])) {
$query
->andWhere('date.city IN (:cityUids)')
->setParameter('cityUids', $filterSettings->getCityUids(), Connection::PARAM_INT_ARRAY)
->setParameter('cityUids', (array)$filterSettings['cityUids'], Connection::PARAM_INT_ARRAY)
;
} elseif (count($filterSettings->getRegionUids()) > 0) {
} elseif (!empty($filterSettings['regionUids'])) {
$query
->andWhere('date.region IN (:regionUids)')
->setParameter('regionUids', $filterSettings->getRegionUids(), Connection::PARAM_INT_ARRAY)
->setParameter('regionUids', (array)$filterSettings['regionUids'], Connection::PARAM_INT_ARRAY)
;
} elseif (count($filterSettings->getCountryUids()) > 0) {
} elseif (!empty($filterSettings['countryUids'])) {
$query
->andWhere('date.country IN (:countryUids)')
->setParameter('countryUids', $filterSettings->getCountryUids(), Connection::PARAM_INT_ARRAY)
->setParameter('countryUids', (array)$filterSettings['countryUids'], Connection::PARAM_INT_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addConceptFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addConceptFilterConditions(QueryBuilder $query, array $filterSettings): void
{
if (count($filterSettings->getConceptUids()) > 0) {
if (!empty($filterSettings['conceptUids'])) {
$query
->andWhere('date.concept IN (:conceptUids)')
->setParameter('conceptUids', $filterSettings->getConceptUids(), Connection::PARAM_INT_ARRAY)
->setParameter('conceptUids', (array)$filterSettings['conceptUids'], Connection::PARAM_INT_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addPaxFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addPaxFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Pax
if ((int) $filterSettings->getPax() > 0) {
if (!empty($filterSettings['pax'])) {
$query
->andWhere('date.available >= :pax')
->setParameter('pax', $filterSettings->getPax())
->setParameter('pax', (int)$filterSettings['pax'])
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addPriceRangeFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addPriceRangeFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Price ranges
if (count($filterSettings->getPriceRanges()) > 0) {
if (!empty($filterSettings['priceRanges'])) {
$orX = $query->expr()->orX();
foreach ($filterSettings->getPriceRanges() as $priceRange) {
foreach ((array)$filterSettings['priceRanges'] as $priceRange) {
[ $priceMin, $priceMax ] = FilterOptions::$priceRangeOptions[$priceRange];
$orX->add($query->expr()->andX(
$query->expr()->gte('date.min_price', ':priceMin'),
@@ -182,79 +158,63 @@ abstract class AbstractRepository extends \TYPO3\CMS\Extbase\Persistence\Reposit
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addDurationFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addDurationFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Duration
if ((int) $filterSettings->getNights() === FilterSettings::DURATION_SHORT) {
if (empty($filterSettings['nights'])) {
return;
}
$nights = (int)$filterSettings['nights'];
if ($nights === FilterSettings::DURATION_SHORT) {
$query
->andWhere('date.nights <= :nights')
->setParameter('nights', $filterSettings->getNights())
->setParameter('nights', $nights)
;
} elseif ((int) $filterSettings->getNights() === FilterSettings::DURATION_LONG) {
} elseif ($nights === FilterSettings::DURATION_LONG) {
$query
->andWhere('date.nights >= :nights')
->setParameter('nights', $filterSettings->getNights())
->setParameter('nights', $nights)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addHotelTypesFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addHotelTypesFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Hotel types
if (count($filterSettings->getHotelTypes()) > 0) {
if (!empty($filterSettings['hotelTypes'])) {
$query
->andWhere('date.hotel_type IN (:hotelTypes)')
->setParameter('hotelTypes', $filterSettings->getHotelTypes(), Connection::PARAM_INT_ARRAY)
->setParameter('hotelTypes', (array)$filterSettings['hotelTypes'], Connection::PARAM_INT_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addBoardTypesFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addBoardTypesFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Board types
if (count($filterSettings->getBoardTypes()) > 0) {
if (!empty($filterSettings['boardTypes'])) {
$query
->andWhere('date.board_bus_pro_id IN (:boardTypes)')
->setParameter('boardTypes', $filterSettings->getBoardTypes(), Connection::PARAM_STR_ARRAY)
->setParameter('boardTypes', (array)$filterSettings['boardTypes'], Connection::PARAM_STR_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addRoomTypesFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addRoomTypesFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Room types
if (count($filterSettings->getRoomTypes()) > 0) {
if (!empty($filterSettings['roomTypes'])) {
$query
->andWhere('room.type IN (:roomTypes)')
->setParameter('roomTypes', $filterSettings->getRoomTypes(), Connection::PARAM_INT_ARRAY)
->setParameter('roomTypes', (array)$filterSettings['roomTypes'], Connection::PARAM_INT_ARRAY)
;
}
}
/**
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
*/
protected function addTransportationFilterConditions(QueryBuilder $query, FilterSettings $filterSettings)
protected function addTransportationFilterConditions(QueryBuilder $query, array $filterSettings): void
{
// Transportation
if ($filterSettings->getBus() === true) {
if (!empty($filterSettings['bus'])) {
$query->andWhere('date.bus_available = 1');
}
}
@@ -263,10 +223,10 @@ abstract class AbstractRepository extends \TYPO3\CMS\Extbase\Persistence\Reposit
* Adds general conditions for provided filtersettings
*
* @param QueryBuilder $query
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param bool $constraintDates
*/
protected function addFiltersettingsConditions(QueryBuilder $query, FilterSettings $filterSettings, $constraintDates = true)
protected function addFiltersettingsConditions(QueryBuilder $query, array $filterSettings, $constraintDates = true)
{
if ($constraintDates) {
$this->addDateFilterConditions($query, $filterSettings);
@@ -281,41 +241,21 @@ abstract class AbstractRepository extends \TYPO3\CMS\Extbase\Persistence\Reposit
$this->addRoomTypesFilterConditions($query, $filterSettings);
$this->addTransportationFilterConditions($query, $filterSettings);
// Default fulltext search in case autocompletion has not been used
// if ((int) $filterSettings->getDestinationUid() === 0 && !empty($filterSettings->getDestinationName())) {
// $query
// ->andWhere($query->expr()->orX(
// $query->expr()->like('LOWER(date.country_name) COLLATE utf8_bin',':destination'),
// $query->expr()->like('LOWER(date.country_keywords) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.region_name) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.region_keywords) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.city_name) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.city_keywords) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.hotel_name) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.hotel_keywords) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.product_name) COLLATE utf8_bin', ':destination'),
// $query->expr()->like('LOWER(date.product_keywords) COLLATE utf8_bin', ':destination')
// ))
// ->setParameter('destination', '%' . mb_strtolower($filterSettings->getDestinationName()) . '%')
// ;
// }
// Product
if ((int) $filterSettings->getProductUid() > 0) {
if (!empty($filterSettings['productUid'])) {
$query
->andWhere('date.product = :productUid')
->setParameter('productUid', $filterSettings->getProductUid())
->setParameter('productUid', (int)$filterSettings['productUid'])
;
}
// Hotel
if ((int) $filterSettings->getHotelUid() > 0) {
if (!empty($filterSettings['hotelUid'])) {
$query
->andWhere('date.hotel = :hotel')
->setParameter(':hotel', $filterSettings->getHotelUid())
->setParameter(':hotel', (int)$filterSettings['hotelUid'])
;
}
}
}
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Domain\Repository;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\Product;
use TYPO3\CMS\Core\Database\Connection;
@@ -73,7 +72,7 @@ class DateRepository extends AbstractRepository
}
public function getFilterOptions(
FilterSettings $filterSettings,
array $filterSettings,
array $excludedConceptUids = [],
array $excludedRegionUids = [],
array $forcedConceptUids = []
@@ -131,11 +130,11 @@ class DateRepository extends AbstractRepository
/**
* @param string $search
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function searchHotels($search, FilterSettings $filterSettings)
public function searchHotels($search, array $filterSettings)
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -166,12 +165,12 @@ class DateRepository extends AbstractRepository
/**
* @param string $search
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param array $excludedConceptUids
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function searchProducts($search, FilterSettings $filterSettings, $excludedConceptUids = [])
public function searchProducts($search, array $filterSettings, $excludedConceptUids = [])
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -215,12 +214,12 @@ class DateRepository extends AbstractRepository
/**
* @param string $type
* @param string $search
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param array $excludedRegionUids
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function searchDestinations($type, $search, FilterSettings $filterSettings, $excludedRegionUids = [])
public function searchDestinations($type, $search, array $filterSettings, $excludedRegionUids = [])
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -28,7 +28,6 @@ namespace EP\EpProducts\Domain\Repository;
***************************************************************/
use EP\EpProducts\Domain\Model\Date;
use EP\EpProducts\Domain\Model\FilterSettings;
use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\Domain\Model\Product;
use TYPO3\CMS\Core\Database\Connection;
@@ -78,12 +77,12 @@ class ProductRepository extends AbstractRepository
}
/**
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param array $excludedConceptUids
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function getSearchResult(FilterSettings $filterSettings, $excludedConceptUids = [])
public function getSearchResult(array $filterSettings, $excludedConceptUids = [])
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -131,13 +130,13 @@ class ProductRepository extends AbstractRepository
}
/**
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param array $excludedConceptUids
* @param int $limit
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function getTeasers(FilterSettings $filterSettings, $excludedConceptUids = [], $limit = 0)
public function getTeasers(array $filterSettings, $excludedConceptUids = [], $limit = 0)
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -173,12 +172,12 @@ class ProductRepository extends AbstractRepository
$query->setMaxResults($limit);
}
if (!$filterSettings->getProductUid()) {
if (empty($filterSettings['productUid'])) {
$query->andWhere('product_searchable = 1');
} else {
$query
->andWhere('product = :productUid')
->setParameter('productUid', $filterSettings->getProductUid())
->setParameter('productUid', (int)$filterSettings['productUid'])
;
}
@@ -195,18 +194,18 @@ class ProductRepository extends AbstractRepository
;
}
return $query->execute()->fetchAllAssociative(\PDO::FETCH_ASSOC);
return $query->execute()->fetchAllAssociative();
}
/**
* @param Product $product
* @param Hotel $hotel
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param Date $date
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function getPricetable(Product $product, Hotel $hotel, FilterSettings $filterSettings, Date $date = null)
public function getPricetable(Product $product, Hotel $hotel, array $filterSettings, Date $date = null)
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -248,11 +247,11 @@ class ProductRepository extends AbstractRepository
/**
* @param Product $product
* @param Hotel $hotel
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @return array
* @throws \Doctrine\DBAL\DBALException
*/
public function getAvailableDates(Product $product, Hotel $hotel, FilterSettings $filterSettings)
public function getAvailableDates(Product $product, Hotel $hotel, array $filterSettings)
{
$qb = $this->getDbConnection()->createQueryBuilder();
@@ -61,22 +61,23 @@ class FilterService implements SingletonInterface
bool $respectBookableDaterange = true,
string $dateFromYmd = null,
string $dateToYmd = null
): FilterSettings {
): array {
$filterSettings = [];
[ $dateFrom, $dateTo ] = $this->constrainDateRange($this->dateRepository->getDateRange());
if (null !== $dateFromYmd) {
$dateFrom = new \DateTime($dateFromYmd);
$filterSettings['dateFrom'] = $dateFromYmd;
} elseif (!$respectBookableDaterange) {
$dateFrom = new \DateTime('now');
$dateFrom = (new \DateTime('now'))->format('Y-m-d');
}
if (null !== $dateToYmd) {
$dateTo = new \DateTime($dateToYmd);
$filterSettings['dateTo'] = $dateToYmd;
}
$filterSettings = new FilterSettings($dateFrom, $dateTo);
if ($forcedConceptUids) {
$filterSettings->setConceptUids($forcedConceptUids);
$filterSettings['conceptUids'] = $forcedConceptUids;
}
return $filterSettings;
@@ -84,7 +85,7 @@ class FilterService implements SingletonInterface
public function getFilterOptions
(
FilterSettings $filterSettings,
array $filterSettings,
array $excludedConceptUids = [],
array $excludedRegionUids = [],
array $forcedConceptUids = []
@@ -250,36 +251,36 @@ class FilterService implements SingletonInterface
return [$dateFrom, $dateTo];
}
public function getFiltersettingsFromSettings(array $settings): FilterSettings
public function getFiltersettingsFromSettings(array $settings): array
{
$filterSettings = new FilterSettings();
$filterSettings = [];
if (isset($settings['regionUid']) && (int) $settings['regionUid'] > 0) {
$filterSettings->setRegionUids([$settings['regionUid']]);
$filterSettings['regionUids'] = [ $settings['regionUid'] ];
} elseif (isset($settings['countryUid']) && (int) $settings['countryUid'] > 0) {
$filterSettings->setCountryUids([$settings['countryUid']]);
$filterSettings['countryUids'] = [ $settings['countryUid'] ];
} elseif (isset($settings['hotelUid']) && (int) $settings['hotelUid'] > 0) {
$filterSettings->setHotelUid($settings['hotelUid']);
$filterSettings['hotelUid'] = $settings['hotelUid'];
}
$conceptUids = GeneralUtility::trimExplode(',', $settings['conceptUid'], true);
if ($conceptUids) {
$filterSettings->setConceptUids($conceptUids);
$filterSettings['conceptUids'] = $conceptUids;
}
if (isset($settings['dateFrom']) && (int) $settings['dateFrom'] > 0) {
$dateFrom = new \DateTime();
$dateFrom->setTimestamp($settings['dateFrom']);
$filterSettings->setDateFrom($dateFrom);
$filterSettings['dateFrom'] = $dateFrom->format('Y-m-d');
}
if (isset($settings['dateTo']) && (int) $settings['dateTo'] > 0) {
$dateTo = new \DateTime();
$dateTo->setTimestamp($settings['dateTo']);
$filterSettings->setDateTo($dateTo);
$filterSettings['dateTo'] = $dateTo->format('Y-m-d');
}
if (isset($settings['busAvailable']) && (bool) $settings['busAvailable'] === true) {
$filterSettings->setBus(true);
$filterSettings['bus'] = true;
}
if (array_key_exists('hotelTypes', $settings)) {
$hotelTypes = GeneralUtility::trimExplode(',', $settings['hotelTypes'], true);
$filterSettings->setHotelTypes($hotelTypes);
$filterSettings['hotelTypes'] = $hotelTypes;
}
return $filterSettings;
@@ -27,7 +27,6 @@ namespace EP\EpProducts\Service;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use EP\EpProducts\Domain\Model\FilterSettings;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class FilterSettingsEncoder
@@ -55,18 +54,20 @@ class FilterSettingsEncoder
];
}
public static function encode(FilterSettings $filterSettings): string
public static function encode(array $filterSettings): string
{
$encoded = [];
foreach (static::getMapping() as $property)
{
foreach (static::getMapping() as $property) {
[$name, $type] = $property;
$method = 'get' . ucfirst($name);
$value = $filterSettings->{$method}();
if (!isset($filterSettings[$name])) {
$encoded[] = '';
continue;
}
$value = $filterSettings[$name];
if ($type === 'array') {
$encoded[] = implode(';', $value);
} elseif ($type === 'date') {
$encoded[] = ($value !== null) ? $value->format('Y-m-d') : '';
$encoded[] = $value ?? '';
} else {
$encoded[] = preg_replace('/[\r\n]/', '', $value);
}
@@ -95,13 +95,13 @@ class ProductDataService implements SingletonInterface
}
/**
* @param FilterSettings $filterSettings
* @param array $filterSettings
* @param array $excludedConceptUids
* @param int $limit
*
* @return array
*/
public function getTeasergroup(FilterSettings $filterSettings, $excludedConceptUids = [], $limit = 0)
public function getTeasergroup(array $filterSettings, $excludedConceptUids = [], $limit = 0)
{
$teaserData = $this->productRepository->getTeasers($filterSettings, $excludedConceptUids);
return $this->preprocessTeasergroup($teaserData, $limit);
@@ -66,7 +66,7 @@ class SearchResultService implements SingletonInterface
$this->flagIconService = $flagIconService;
}
public function getResult(FilterSettings $filterSettings, array $excludedConceptUids = []): SearchResult
public function getResult(array $filterSettings, array $excludedConceptUids = []): SearchResult
{
$searchResultData = $this->productRepository->getSearchResult($filterSettings, $excludedConceptUids);
@@ -48,16 +48,10 @@ class SearchResultUrlService implements SingletonInterface
/**
* @param SearchResult $searchResult
* @param array $options
* @param int $defaultDetailPageUid
*/
public function process(SearchResult $searchResult, array $options): void
public function process(SearchResult $searchResult, $defaultDetailPageUid): void
{
$optionsResolver = new OptionsResolver();
$optionsResolver->setRequired([ 'defaultDetailPageUid' ]);
$resolvedOptions = $optionsResolver->resolve($options);
$defaultDetailPageUid = $resolvedOptions['defaultDetailPageUid'];
$data = $searchResult->getData();
foreach ($data as $conceptUid => $conceptData) {
@@ -32,19 +32,21 @@ class DateUtility
public static function formatDateRange
(
\DateTime $dateFrom = null,
\DateTime $dateTo = null,
?string $dateFromString = '',
?string $dateToString = '',
bool $includeLabel = true,
string $format = 'd.m.Y'
): string {
if ($dateFrom === null && $dateTo === null) {
if (empty($dateFromString) && empty($dateToString)) {
return '';
}
$dateFrom = new \DateTime($dateFromString);
$dateTo = new \DateTime($dateToString);
$label = $includeLabel ? 'im Zeitraum ' : '';
if ($dateFrom !== null && $dateTo === null) {
if ($dateFromString !== '' && $dateToString === '') {
return $label . 'ab ' . $dateFrom->format($format);
}
if ($dateFrom === null && $dateTo !== null) {
if ($dateFromString === '' && $dateToString !== '') {
return $label . '-' . $dateTo->format($format);
}
return $label . $dateFrom->format($format) . '-' . $dateTo->format($format);
@@ -40,8 +40,8 @@ class DateRangeViewHelper extends AbstractViewHelper
{
parent::initializeArguments();
$this->registerArgument('dateFrom', 'mixed', 'Date range start');
$this->registerArgument('dateTo', 'mixed', 'Date range end');
$this->registerArgument('dateFrom', 'string', 'Date range start');
$this->registerArgument('dateTo', 'string', 'Date range end');
$this->registerArgument('includeLabel', 'Bool', 'Whether to include a label', false, true);
$this->registerArgument('format', 'String', 'The date format', false, 'd.m.Y');
}
@@ -56,27 +56,12 @@ class DateRangeViewHelper extends AbstractViewHelper
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
)
{
$dateFrom = static::ensureDateObject($arguments['dateFrom']);
$dateTo = static::ensureDateObject($arguments['dateTo']);
) {
return DateUtility::formatDateRange(
$dateFrom,
$dateTo,
$arguments['dateFrom'],
$arguments['dateTo'],
$arguments['includeLabel'],
$arguments['format']
);
}
private static function ensureDateObject($argument): \DateTime
{
if ($argument instanceof \DateTime) {
$dateTime = $argument;
} else {
$dateTime = new \DateTime($argument);
}
return $dateTime;
}
}
@@ -14,21 +14,12 @@ import Alpine from 'alpinejs'
import parseAppConfig from './components/appconfig'
import initSliders from './components/sliders'
import appendPaCode from './components/pacode'
import scrollTop from './components/scoll-top'
import scrollTo from './components/scroll-to'
import sticky from './components/sticky'
import watchlistToggle from './components/watchlist-toggle'
import watchlistButton from './components/watchlist-button'
import watchlist from './components/watchlist'
import collapse from './components/collapse'
import fbpixel from './components/fbpixel'
import ajaxContent from './components/ajax-content'
import faq from './components/faq'
import osmMap from './components/osm-map'
import productDetail from './components/product-detail'
import daytripDateSelect from './components/daytrip-date-select'
import searchBar from './components/search-bar'
import search from './components/search'
import calendar from './components/calendar'
import contactForm from './components/contact-form'
@@ -54,21 +45,12 @@ Alpine.store('show', {
Alpine.store('watchlist', storedWatchlist)
Alpine.store('filterSettings', defaultFilterSettings)
Alpine.store('filterOptions', defaultFilterOptions)
Alpine.data('scrollTop', scrollTop)
Alpine.data('scrollTo', scrollTo)
Alpine.data('sticky', sticky)
Alpine.data('watchlistToggle', watchlistToggle)
Alpine.data('watchlistButton', watchlistButton)
Alpine.data('watchlist', watchlist)
Alpine.data('collapse', collapse)
Alpine.data('fbpixel', fbpixel)
Alpine.data('ajaxContent', ajaxContent)
Alpine.data('faq', faq)
Alpine.data('osmMap', osmMap)
Alpine.data('productDetail', productDetail)
Alpine.data('daytripDateSelect', daytripDateSelect)
Alpine.data('searchBar', searchBar)
Alpine.data('search', search)
Alpine.data('calendar', calendar)
Alpine.data('contactForm', contactForm)
Alpine.start()
@@ -77,3 +59,10 @@ Glightbox({
selector: 'data-glightbox',
slideEffect: 'fade',
})
import { Application } from 'stimulus'
import { definitionsFromContext } from 'stimulus/webpack-helpers'
const application = Application.start()
const context = require.context('./controllers', true, /\.js$/)
application.load(definitionsFromContext(context))
@@ -1,44 +0,0 @@
import axios from 'axios'
import { debounce } from './throttle-debounce'
export default uri => ({
uri,
loaded: false,
loading: false,
init() {
window.addEventListener('scroll', debounce(() => {
if (true === this.loaded) {
return
}
this.load()
}, 250))
},
load() {
if (false === this.inViewport()) {
return
}
this.loading = true
axios({
url: this.uri,
responseType: 'text'
}).then((response) => {
this.loaded = true
this.$refs.container.innerHTML = response.data
this.$refs.trigger.remove()
}).catch(() => {
this.loaded = false
}).finally(() => {
this.loading = false
})
},
inViewport () {
const rect = this.$refs.trigger.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
}
})
@@ -1,31 +0,0 @@
import { debounce } from './throttle-debounce'
export default (isCollapsed = true, until = null, from = null) => ({
collapsed: isCollapsed,
collapsedUntil: until,
collapsedFrom: from,
init() {
this.checkBreakpoints()
window.addEventListener('resize', debounce(() => {
this.checkBreakpoints()
}, 250))
},
checkBreakpoints() {
if (null !== this.collapsedUntil) {
this.collapsed = window.innerWidth < parseInt(this.collapsedUntil)
}
if (null !== this.collapsedFrom) {
this.collapsed = window.innerWidth > parseInt(this.collapsedFrom)
}
},
trigger: {
['@click.prevent']() {
this.collapsed = !this.collapsed
},
},
container: {
['x-show']() {
return this.collapsed === false
},
}
})
@@ -1,24 +0,0 @@
import { debounce } from './throttle-debounce'
export default () => ({
query: '',
init() {
this.entries = this.$el.querySelectorAll('[x-data^="collapse"]')
this.$watch('query', debounce(value => {
if (value.length < 3) {
this.entries.forEach(entry => {
entry.classList.remove('hidden')
})
return
}
const pattern = new RegExp(value, 'i')
this.entries.forEach(entry => {
const question = entry.childNodes[0].textContent
const answer = entry.childNodes[1].textContent
if (!question.match(pattern) && !answer.match(pattern)) {
entry.classList.add('hidden')
}
})
}, 200))
},
})
@@ -1,5 +0,0 @@
export default props => ({
init() {
fbq('track', 'ViewContent', props)
}
})
@@ -1,52 +0,0 @@
import L from 'leaflet';
export default () => ({
init() {
this.lat = this.$el.dataset.lat
this.lng = this.$el.dataset.lng
this.zoom = this.$el.dataset.zoom || 12
this.externalLink = this.$el.dataset.link
this.markers = this.$el.dataset.markers ? JSON.parse(this.$el.dataset.markers) : []
this.map = L.map(this.$el, {
scrollWheelZoom: false
});
this.map.setZoom(this.zoom);
if (this.lat && this.lng) {
const position = L.latLng(this.lat, this.lng);
const marker = L.marker(position);
if (this.externalLink) {
marker.on('click', () => {
window.open(this.externalLink, '_blank')
});
} else {
marker.on('click', () => {
window.open('https://www.google.com/maps/search/?api=1&zoom=15&query=' + this.lat + ',' + this.lng, '_blank')
});
}
marker.addTo(this.map);
this.map.setView(position);
} else if (this.markers.length) {
const bounds = L.latLngBounds();
for (let item of this.markers) {
const position = L.latLng(item.lat, item.lng);
const marker = L.marker(position);
if (item.externalLink) {
marker.on('click', () => {
window.open(item.externalLink, '_blank');
});
} else {
const popupContent = '<div><strong>' + item.name + '</strong><br>' + item.teaser +
'<br><a href="' + item.uri + '">zu den Details</a></div>';
marker.bindPopup(popupContent);
}
marker.addTo(this.map);
bounds.extend(position);
}
this.map.fitBounds(bounds);
}
L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map);
}
})
@@ -1,18 +0,0 @@
import { debounce } from './throttle-debounce'
export default () => ({
show: false,
trigger: {
['@click.prevent']() {
window.scroll({top: 0, left: 0, behavior: 'smooth'})
},
['x-show']() {
return this.show
}
},
init() {
window.addEventListener('scroll', debounce(() => {
this.show = window.scrollY > window.innerHeight / 3
}, 250))
},
})
@@ -1,9 +0,0 @@
export default targetId => ({
targetId,
init() {
this.$el.addEventListener('click', e => {
e.preventDefault()
document.getElementById(this.targetId).scrollIntoView({behavior: 'smooth'})
})
}
})
@@ -1,11 +0,0 @@
import { debounce } from './throttle-debounce'
export default offset => ({
offset,
sticky: false,
init() {
window.addEventListener('scroll', debounce(() => {
this.sticky = window.scrollY > this.offset
}, 250))
}
})
@@ -0,0 +1,72 @@
import { Controller } from 'stimulus'
import { useDebounce } from 'stimulus-use'
import { useFetch} from '../mixins/use_fetch'
export default class extends Controller {
static debounces = [ 'load' ]
static values = {
uri: String,
loading: Boolean,
loaded: Boolean,
}
static targets = [ 'container', 'trigger' ]
initialize() {
useDebounce(this)
useFetch(this)
}
connect() {
window.addEventListener('scroll', () => {
this.load()
})
}
disconnect() {
window.removeEventListener('scroll', () => {
this.load()
})
}
load() {
if (true === this.loadedValue) {
return
}
if (false === this.inViewport()) {
return
}
this.loadHtml()
}
loadHtml() {
this.loadingValue = true
fetch(this.uriValue)
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.loadedValue = true
this.containerTarget.innerHTML = html
this.triggerTarget.remove()
})
.catch(() => {
this.loadedValue = false
})
.finally(() => {
this.loadingValue = false
})
}
inViewport() {
const rect = this.triggerTarget.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
}
}
@@ -0,0 +1,48 @@
import { Controller } from 'stimulus'
import { useWindowResize, useDebounce } from 'stimulus-use'
export default class extends Controller {
static debounces = [ 'checkBreakpoints' ]
static values = {
collapsed: Boolean,
from: Number,
until: Number,
}
static targets = [ 'container', 'icon' ]
initialize() {
useWindowResize(this)
useDebounce(this)
}
connect() {
this.toggleByWindowWidth(window.innerWidth)
}
windowResize({ width, height, event }) {
this.toggleByWindowWidth(width)
}
toggle() {
this.collapsedValue = !this.collapsedValue
}
toggleByWindowWidth(width) {
if (0 !== this.untilValue) {
this.collapsedValue = width < this.untilValue
}
if (0 !== this.fromValue) {
this.collapsedValue = width > this.fromValue
}
}
collapsedValueChanged() {
this.containerTarget.classList.toggle('hidden', this.collapsedValue === true)
if (this.hasIconTarget) {
this.iconTarget.classList.toggle('rotate-180', this.collapsedValue === false)
}
}
}
@@ -0,0 +1,56 @@
import { Controller } from 'stimulus'
import { useDispatch } from 'stimulus-use'
import flatpickr from 'flatpickr'
import { German } from 'flatpickr/dist/l10n/de'
export default class extends Controller {
static targets = [ 'field' ]
static values = {
minDate: String,
dateFrom: String,
dateTo: String,
}
initialize() {
useDispatch(this)
this.picker = flatpickr(this.fieldTarget, {
mode: 'range',
locale: German,
inline: true,
minDate: this.minDateValue,
onChange: selectedDates => {
if (selectedDates.length === 2) {
this.dispatch('range-select', {
from: flatpickr.formatDate(selectedDates[0], 'Y-m-d'),
to: flatpickr.formatDate(selectedDates[1], 'Y-m-d'),
})
}
}
})
}
connect() {
if ('' !== this.dateFromValue && '' !== this.dateToValue) {
let dateFrom = new Date(this.dateFromValue)
let dateTo = new Date(this.dateToValue)
this.picker.setDate([ dateFrom, dateTo ])
}
}
selectPreset(e) {
e.preventDefault()
const preset = JSON.parse(e.currentTarget.dataset.preset)
this.picker.setDate([ new Date(preset.dateFrom), new Date(preset.dateTo) ])
this.dispatch('range-select', { from: preset.dateFrom, to: preset.dateTo })
}
reset(e) {
e.preventDefault()
this.picker.clear()
this.picker.jumpToDate()
this.dispatch('range-select', { from: null, to: null })
}
}
@@ -0,0 +1,31 @@
import { Controller } from 'stimulus'
import { useDebounce } from 'stimulus-use'
export default class extends Controller {
static debounces = [ 'filter' ]
static targets = [ 'entry' ]
initialize() {
useDebounce(this)
}
filter(e) {
let value = e.target.value
if (value.length < 3) {
this.entryTargets.forEach(entry => {
entry.classList.remove('hidden')
})
return
}
const pattern = new RegExp(value, 'i')
this.entryTargets.forEach(entry => {
const question = entry.childNodes[0].textContent
const answer = entry.childNodes[1].textContent
if (!question.match(pattern) && !answer.match(pattern)) {
entry.classList.add('hidden')
}
})
}
}
@@ -0,0 +1,11 @@
import { Controller } from 'stimulus'
export default class extends Controller {
static values = { props: Object }
connect() {
fbq('track', 'ViewContent', this.propsValue)
}
}
@@ -0,0 +1,58 @@
import { Controller } from 'stimulus'
import L from 'leaflet'
export default class extends Controller {
static values = {
lat: Number,
lng: Number,
zoom: Number,
externalLink: String,
markers: Array,
}
connect() {
this.map = L.map(this.element, {
scrollWheelZoom: false
})
const zoom = this.hasZoomValue ? this.zoomValue : 12
this.map.setZoom(zoom)
if (this.hasLatValue && this.hasLngValue) {
const position = L.latLng(this.latValue, this.lngValue)
const marker = L.marker(position)
if (this.hasExternalLinkValue) {
marker.on('click', () => {
window.open(this.externalLinkValue, '_blank')
})
} else {
marker.on('click', () => {
window.open('https://www.google.com/maps/search/?api=1&zoom=15&query=' + this.latValue + ',' + this.lngValue, '_blank')
})
}
marker.addTo(this.map)
this.map.setView(position)
} else if (this.hasMarkersValue && this.markersValue.length) {
const bounds = L.latLngBounds()
for (let item of this.markers) {
const position = L.latLng(item.lat, item.lng)
const marker = L.marker(position)
if (item.externalLink) {
marker.on('click', () => {
window.open(item.externalLink, '_blank')
})
} else {
const popupContent = '<div><strong>' + item.name + '</strong><br>' + item.teaser +
'<br><a href="' + item.uri + '">zu den Details</a></div>'
marker.bindPopup(popupContent)
}
marker.addTo(this.map)
bounds.extend(position)
}
this.map.fitBounds(bounds)
}
L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map)
}
}
@@ -0,0 +1,10 @@
import { Controller } from 'stimulus'
export default class extends Controller {
static values = { target: String }
scroll() {
document.getElementById(this.targetValue).scrollIntoView({ behavior: 'smooth' })
}
}
@@ -0,0 +1,25 @@
import { Controller } from 'stimulus'
import { useDebounce} from 'stimulus-use'
export default class extends Controller {
static debounces = [ 'toggle' ]
initialize() {
useDebounce(this)
}
connect() {
window.addEventListener('scroll', () => {
this.toggle()
})
}
toggle() {
this.element.classList.toggle('hidden', window.scrollY < window.innerHeight / 3)
}
scroll() {
window.scroll({ top: 0, left: 0, behavior: 'smooth' })
}
}
@@ -0,0 +1,53 @@
import { Controller } from 'stimulus'
import { useDispatch } from 'stimulus-use'
import { useFetch } from '../mixins/use_fetch'
export default class extends Controller {
static values = {
loading: Boolean,
uri: String,
}
static targets = [
'form',
'resultsContainer',
'loadingIndicator',
'dateFrom',
'dateTo',
]
initialize() {
useFetch(this)
useDispatch(this)
}
filter() {
this.loadingValue = true
let data = new FormData(this.formTarget)
fetch(this.uriValue, { method: 'POST', body: data })
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.resultsContainerTarget.innerHTML = html
})
.finally(() => {
this.loadingValue = false
this.element.scrollIntoView({ behavior: 'smooth' })
})
}
reset() {
this.filter()
}
selectDateRange(e) {
this.dateFromTarget.value = e.detail.from
this.dateToTarget.value = e.detail.to
this.filter()
}
loadingValueChanged() {
this.loadingIndicatorTarget.classList.toggle('hidden', this.loadingValue === false)
}
}
@@ -0,0 +1,38 @@
import { Controller } from 'stimulus'
import { useDebounce } from 'stimulus-use'
export default class extends Controller {
static debounces = [ 'toggle' ]
static classes = [ 'sticky' ]
static values = {
sticky: Boolean,
offset: Number,
}
initialize() {
useDebounce(this)
}
connect() {
window.addEventListener('scroll', () => {
this.toggle()
})
}
disconnect() {
window.removeEventListener('scroll', () => {
this.toggle()
})
}
toggle() {
this.stickyValue = window.scrollY > this.offsetValue
}
stickyValueChanged() {
this.element.classList.toggle(this.stickyClass, this.stickyValue)
}
}
@@ -0,0 +1,11 @@
export const useConfig = controller => {
Object.assign(controller, {
getConfig() {
const configElement = document.getElementById('appconfig')
if (null === configElement) {
return {}
}
return JSON.parse(configElement.innerHTML)
}
})
}
@@ -0,0 +1,11 @@
export const useDebounce = controller => {
Object.assign(controller, {
debounce(callback, interval) {
let debounceId
return (...args) => {
clearTimeout(debounceId)
debounceId = setTimeout(() => callback.apply(this, args), interval)
}
}
});
};
@@ -0,0 +1,21 @@
import 'whatwg-fetch'
export const useFetch = controller => {
Object.assign(controller, {
checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
const error = new Error(response.statusText)
error.response = response
throw error
}
},
parseJSON(response) {
return response.json()
},
parseHTML(response) {
return response.text()
}
})
}
@@ -1,27 +1,11 @@
.bg-winter {
@apply relative bg-cover bg-no-repeat bg-center;
@apply bg-cover bg-no-repeat bg-center;
background-image: url(../../images/background/winter.jpg);
z-index: -5;
&:before {
@apply block absolute top-0 left-0 inset-0 opacity-80;
@apply bg-ep-primary sbw:bg-sbw-primary uch:bg-uch-primary;
z-index: -10;
content: '';
}
}
.bg-pattern {
@apply relative bg-cover bg-no-repeat bg-center;
@apply bg-cover bg-no-repeat bg-center;
background-image: url(../../images/background/pattern.jpg);
z-index: -5;
&:before {
@apply block absolute top-0 left-0 inset-0 opacity-80;
@apply bg-ep-primary sbw:bg-sbw-primary uch:bg-uch-primary;
z-index: -10;
content: '';
}
}
.bg-header-season-w {
@@ -4,64 +4,66 @@
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="bg-winter my-2">
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
</f:if>
<div class="relative container grid grid-cols-2 gap-8" data-rte-content>
<div class="col-span-2 md:col-span-1">
<div class="relative max-w-md mx-auto mb-8">
<f:if condition="{image}">
<f:then>
<img class="block rounded-full w-full h-auto lazyload"
data-src="{f:uri.image(image: image, width: '400', height: '400c')}"
alt="{image.alternative}" title="{image.title}">
</f:then>
<f:else>
<img src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
rel="nofollow"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 -mb-8 rounded-full py-4 px-6 bg-ep-secondary sbw:bg-sbw-secondary uch:bg-uch-secondary flex items-center space-x-2">
<svg class="w-10 h-10 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-center text-white">
<div class="bg-ep-primary-80">
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
</f:if>
<div class="relative container grid grid-cols-2 gap-8" data-rte-content>
<div class="col-span-2 md:col-span-1">
<div class="relative max-w-md mx-auto mb-8">
<f:if condition="{image}">
<f:then>
<img class="block rounded-full w-full h-auto lazyload"
data-src="{f:uri.image(image: image, width: '400', height: '400c')}"
alt="{image.alternative}" title="{image.title}">
</f:then>
<f:else>
<img src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
rel="nofollow"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 -mb-8 rounded-full py-4 px-6 bg-ep-secondary sbw:bg-sbw-secondary uch:bg-uch-secondary flex items-center space-x-2">
<svg class="w-10 h-10 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-center text-white">
<span class="block whitespace-nowrap">
Ihr habt Fragen?
</span>
<span class="block whitespace-nowrap">
<span class="block whitespace-nowrap">
Wir helfen euch gerne!
</span>
</div>
</a>
</div>
</a>
</div>
</div>
</div>
<div class="col-span-2 md:col-span-1">
<div class="col-span-2 md:col-span-1">
<span class="headline--secondary font-light text-white">
{data.subheadline}
</span>
<span class="headline--secondary uppercase font-light text-white mb-4">
<span class="headline--secondary uppercase font-light text-white mb-4">
{data.headline}
</span>
<div class="text-white" data-rte-content>
{data.bodytext -> f:format.html()}
</div>
<f:if condition="{data.flexForm.buttons}">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-8">
<f:for each="{data.flexForm.buttons}" as="button">
<f:link.typolink parameter="{button.container.link}"
class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:for>
<div class="text-white" data-rte-content>
{data.bodytext -> f:format.html()}
</div>
</f:if>
<f:if condition="{data.flexForm.buttons}">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-8">
<f:for each="{data.flexForm.buttons}" as="button">
<f:link.typolink parameter="{button.container.link}"
class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:for>
</div>
</f:if>
</div>
</div>
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</f:if>
</div>
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</f:if>
</div>
</html>
@@ -3,39 +3,41 @@
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="bg-winter my-2">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
<div class="max-w-screen-lg mx-auto" data-rte-content>
<div class="md:flex md:items-start py-4">
<div class="px-16 pb-4 md:px-4 md:pb-0 md:w-1/6">
<f:if condition="{image}">
<f:then>
<img class="block w-full h-auto rounded-full lazyload"
data-src="{f:uri.image(image: image, width: '400', height: '400c')}"
alt="{image.alternative}" title="{image.title}">
</f:then>
<f:else>
<img class="block w-full h-auto rounded-full" src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
</div>
<div class="px-16 md:px-4 md:w-5/6 md:pl-8 text-center text-white md:text-left" data-rte-content>
<div class="bg-ep-primary-80 sbw:bg-sbw-primary-80 uch:bg-uch-primary-80">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
<div class="max-w-screen-lg mx-auto" data-rte-content>
<div class="md:flex md:items-start py-4">
<div class="px-16 pb-4 md:px-4 md:pb-0 md:w-1/6">
<f:if condition="{image}">
<f:then>
<img class="block w-full h-auto rounded-full lazyload"
data-src="{f:uri.image(image: image, width: '400', height: '400c')}"
alt="{image.alternative}" title="{image.title}">
</f:then>
<f:else>
<img class="block w-full h-auto rounded-full" src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
</div>
<div class="px-16 md:px-4 md:w-5/6 md:pl-8 text-center text-white md:text-left" data-rte-content>
<span class="headline--3 text-white uppercase">
{data.headline}
</span>
{data.text -> f:format.html()}
<div class="flex flex-col space-y-4 md:flex-row md:items-center md:space-y-0 md:space-x-4">
<f:for each="{data.flexForm.buttons}" as="button" iteration="iteration">
<f:if condition="{iteration.cycle} <= 3">
<f:link.typolink parameter="{button.container.link}" class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:if>
</f:for>
{data.text -> f:format.html()}
<div class="flex flex-col space-y-4 md:flex-row md:items-center md:space-y-0 md:space-x-4">
<f:for each="{data.flexForm.buttons}" as="button" iteration="iteration">
<f:if condition="{iteration.cycle} <= 3">
<f:link.typolink parameter="{button.container.link}" class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:if>
</f:for>
</div>
</div>
</div>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
</html>
@@ -5,65 +5,66 @@
<div class="bg-winter my-2">
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
</f:if>
<div class="relative container grid grid-cols-2 gap-8 py-8" data-rte-content>
<div class="col-span-2 md:col-span-1">
<div class="relative max-w-md mx-auto mb-8">
<f:if condition="{teaserImage}">
<f:then>
<img class="block rounded-full w-full h-auto lazyload"
data-src="{f:uri.image(image: teaserImage, width: '400', height: '400c')}"
alt="{teaserImage.alternative}" title="{teaserImage.title}">
</f:then>
<f:else>
<img src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
rel="nofollow"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 -mb-8 rounded-full py-4 px-6 bg-ep-secondary sbw:bg-sbw-secondary uch:bg-uch-secondary flex items-center space-x-2">
<svg class="w-10 h-10 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-center text-white">
<div class="bg-ep-primary-80">
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
</f:if>
<div class="relative container grid grid-cols-2 gap-8 py-8" data-rte-content>
<div class="col-span-2 md:col-span-1">
<div class="relative max-w-md mx-auto mb-8">
<f:if condition="{teaserImage}">
<f:then>
<img class="block rounded-full w-full h-auto lazyload"
data-src="{f:uri.image(image: teaserImage, width: '400', height: '400c')}"
alt="{teaserImage.alternative}" title="{teaserImage.title}">
</f:then>
<f:else>
<img src="https://placehold.it/800x600" alt="">
</f:else>
</f:if>
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
rel="nofollow"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 -mb-8 rounded-full py-4 px-6 bg-ep-secondary sbw:bg-sbw-secondary uch:bg-uch-secondary flex items-center space-x-2">
<svg class="w-10 h-10 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-center text-white">
<span class="block whitespace-nowrap">
Ihr habt Fragen?
</span>
<span class="block whitespace-nowrap">
<span class="block whitespace-nowrap">
Wir helfen euch gerne!
</span>
</div>
</a>
</div>
</a>
</div>
</div>
</div>
<div class="col-span-2 md:col-span-1">
<div class="col-span-2 md:col-span-1">
<span class="headline--secondary font-light text-white">
{data.subheader}
</span>
<span class="headline--secondary uppercase font-light text-white mb-4">
<span class="headline--secondary uppercase font-light text-white mb-4">
{data.header}
</span>
<div class="text-white" data-rte-content>
{data.bodytext -> f:format.html()}
</div>
<f:if condition="{data.flexForm.buttons}">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-8">
<f:for each="{data.flexForm.buttons}" as="button">
<f:link.typolink parameter="{button.container.link}"
class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:for>
<div class="text-white" data-rte-content>
{data.bodytext -> f:format.html()}
</div>
</f:if>
<f:if condition="{data.flexForm.buttons}">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-8">
<f:for each="{data.flexForm.buttons}" as="button">
<f:link.typolink parameter="{button.container.link}"
class="button shadow-lg">
{button.container.label}
</f:link.typolink>
</f:for>
</div>
</f:if>
</div>
</div>
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block sbw:md:hidden transform translate-y-2 text-white'}"/>
</f:if>
</div>
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block sbw:md:hidden transform translate-y-2 text-white'}"/>
</f:if>
</div>
</html>
@@ -1,58 +1,57 @@
<f:if condition="{faqs}">
<div class="bg-winter my-2">
<f:render partial="Disrupter/Outlines" section="Top2" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
<div class="container" x-data="faq">
<span class="headline--3 text-white uppercase mb-4">
Häufig gestellte Fragen
</span>
<f:if condition="{filter}">
<div class="mb-4 pr-2 w-80 border-b border-r border-white flex items-center justify-between">
<input type="text"
x-model="query"
class="bg-transparent border-0 uppercase ring-0 focus:ring-0 placeholder-white text-white"
placeholder="Suchbegriff">
<svg class="w-6 h-6 text-white">
<use href="#icon-search"></use>
</svg>
</div>
</f:if>
<f:for each="{faqs}" as="faq" iteration="iteration">
<div class="p-4 rounded mb-2"
x-data="collapse(<f:format.raw>{f:if(condition: iteration.isFirst, then: 'false', else: 'true')}</f:format.raw>)"
x-bind:class="collapsed || 'bg-ep-primary-80'">
<button class="uppercase text-lg text-white flex items-center space-x-4" x-bind="trigger">
<svg class="w-4 h-4 text-white transform transition duration-200 ease-in-out" x-bind:class="collapsed || 'rotate-180'">
<use href="#icon-chevron-down"></use>
<div class="bg-ep-primary-80 sbw:bg-sbw-primary-80 uch:bg-uch-primary-80">
<f:render partial="Disrupter/Outlines" section="Top2" arguments="{classes: 'hidden md:block text-white'}"/>
<div class="container"
data-controller="faq">
<span class="headline--3 text-white uppercase mb-4">
Häufig gestellte Fragen
</span>
<f:if condition="{filter}">
<div class="mb-4 pr-2 w-80 border-b border-r border-white flex items-center justify-between">
<input type="text"
data-action="faq#filter"
class="bg-transparent border-0 uppercase ring-0 focus:ring-0 placeholder-white text-white"
placeholder="Suchbegriff">
<svg class="w-6 h-6 text-white">
<use href="#icon-search"></use>
</svg>
<span>
</div>
</f:if>
<f:for each="{faqs}" as="faq" iteration="iteration">
<div class="p-4 rounded mb-2"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: iteration.isFirst, then: 'false', else: 'true')}"
data-faq-target="entry">
<button class="uppercase text-lg text-white flex items-center space-x-4"
data-action="collapse#toggle">
<svg class="w-4 h-4 text-white transform transition duration-200 ease-in-out"
data-collapse-target="icon">
<use href="#icon-chevron-down"></use>
</svg>
<span>
{faq.question}
</span>
</button>
<div class="text-white pt-2"
x-bind="container"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak
data-rte-content>
{faq.answer -> f:format.html()}
</button>
<div class="text-white pt-2 transition-transform ease-out overflow-hidden origin-top transform hidden"
data-collapse-target="container"
data-rte-content>
{faq.answer -> f:format.html()}
</div>
</div>
</div>
</f:for>
<f:if condition="{settings.infoPageUid}">
<div class="flex items-center justify-end space-x-2 text-sm text-white">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
<a href="{f:uri.typolink(parameter: settings.infoPageUid)}">
Weitere Reiseinfos findet ihr <span class="underline">hier</span>
</a>
</div>
</f:if>
</f:for>
<f:if condition="{settings.infoPageUid}">
<div class="flex items-center justify-end space-x-2 text-sm text-white">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
<a href="{f:uri.typolink(parameter: settings.infoPageUid)}">
Weitere Reiseinfos findet ihr <span class="underline">hier</span>
</a>
</div>
</f:if>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom2" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom2" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
</f:if>
@@ -4,93 +4,95 @@
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="bg-winter mt-8">
<f:if condition="{settings.themeKey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'transform -translate-y-2 text-white'}"/>
</f:if>
<div class="py-4 relative">
<div class="container grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 pb-4">
<v:menu pageUid="{settings.footerNavPid}" levels="2" expandAll="true">
<f:for each="{menu}" as="menuheader" iteration="iteration">
<div x-data="collapse(true, 991)">
<span class="headline--4 text-white hidden md:block">
{menuheader.title}
</span>
<button class="headline--4 text-white md:hidden"
x-bind="trigger">
{menuheader.title}
</button>
<v:menu pageUid="{menuheader.uid}" useShortcutUid="true" as="submenu">
<ul class="list-disc text-white mb-0 pl-4"
x-bind="container"
x-transition>
<f:for each="{submenu}" as="submenuitem">
<f:if condition="{submenuitem.doktype} == 3">
<f:then>
<li>
<a class="text-white" href="{submenuitem.url}" target="_blank" title="{submenuitem.linktext}">{submenuitem.title}</a>
</li>
</f:then>
<f:else>
<li>
<a class="text-white" href="{f:uri.typolink(parameter: submenuitem.uid)}" title="{submenuitem.linktext}">{submenuitem.title}</a>
</li>
</f:else>
</f:if>
</f:for>
</ul>
</v:menu>
</div>
</f:for>
</v:menu>
</div>
<div class="hidden md:block container border-t border-white pt-4">
<div class="grid grid-cols-2 gap-16">
<div class="flex">
<div class="flex items-start space-x-2 pr-4">
<svg class="w-12 h-12 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-white">
<span class="block uppercase text-xl">
Service-Telefon
<div class="bg-ep-primary-80 sbw:bg-sbw-primary-80 uch:bg-uch-primary-80">
<f:if condition="{settings.themekey} == 'ep'">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'transform -translate-y-2 text-white'}"/>
</f:if>
<div class="py-4 relative">
<div class="container grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 pb-4">
<v:menu pageUid="{settings.footerNavPid}" levels="2" expandAll="true">
<f:for each="{menu}" as="menuheader" iteration="iteration">
<div data-controller="collapse"
data-collapse-until-value="991">
<span class="headline--4 text-white hidden md:block">
{menuheader.title}
</span>
<button class="headline--4 text-white md:hidden"
data-action="collapse#toggle">
{menuheader.title}
</button>
<v:menu pageUid="{menuheader.uid}" useShortcutUid="true" as="submenu">
<ul class="list-disc text-white mb-0 pl-4"
data-collapse-target="container">
<f:for each="{submenu}" as="submenuitem">
<f:if condition="{submenuitem.doktype} == 3">
<f:then>
<li>
<a class="text-white" href="{submenuitem.url}" target="_blank" title="{submenuitem.linktext}">{submenuitem.title}</a>
</li>
</f:then>
<f:else>
<li>
<a class="text-white" href="{f:uri.typolink(parameter: submenuitem.uid)}" title="{submenuitem.linktext}">{submenuitem.title}</a>
</li>
</f:else>
</f:if>
</f:for>
</ul>
</v:menu>
</div>
</f:for>
</v:menu>
</div>
<div class="hidden md:block container border-t border-white pt-4">
<div class="grid grid-cols-2 gap-16">
<div class="flex">
<div class="flex items-start space-x-2 pr-4">
<svg class="w-12 h-12 text-white">
<use href="#icon-phone"></use>
</svg>
<div class="text-white">
<span class="block uppercase text-xl">
Service-Telefon
</span>
<a class="text-white whitespace-nowrap"
href="tel:{settings.phoneNumberLink}"
title="Service Telefon anrufen"
rel="nofollow">
{settings.phoneNumber}
</a>
</div>
</div>
<div class="text-white flex-1">
<span class="block">
{settings.businessHoursDays}
</span>
<span class="block">
{settings.businessHoursTime}
</span>
<a class="text-white whitespace-nowrap"
href="tel:{settings.phoneNumberLink}"
title="Service Telefon anrufen"
rel="nofollow">
{settings.phoneNumber}
</a>
</div>
</div>
<div class="text-white flex-1">
<span class="block">
{settings.businessHoursDays}
</span>
<span class="block">
{settings.businessHoursTime}
</span>
</div>
</div>
<div class="flex">
<div class="flex items-start space-x-2 pr-4">
<svg class="w-12 h-12 text-white">
<use href="#icon-whatsapp"></use>
</svg>
<div class="text-white">
<span class="block uppercase text-xl">
WhatsApp
</span>
<a class="text-white whitespace-nowrap"
href="https://api.whatsapp.com/send?phone={settings.whatsAppNumberLink}"
title="WhatsApp"
target="_blank"
rel="nofollow">
{settings.whatsApp}
</a>
<div class="flex">
<div class="flex items-start space-x-2 pr-4">
<svg class="w-12 h-12 text-white">
<use href="#icon-whatsapp"></use>
</svg>
<div class="text-white">
<span class="block uppercase text-xl">
WhatsApp
</span>
<a class="text-white whitespace-nowrap"
href="https://api.whatsapp.com/send?phone={settings.whatsAppNumberLink}"
title="WhatsApp"
target="_blank"
rel="nofollow">
{settings.whatsApp}
</a>
</div>
</div>
<div class="text-white flex-1">
{f:translate(key: 'label.whatsapp_instructions', extensionName: 'ep_theme')}
</div>
</div>
<div class="text-white flex-1">
{f:translate(key: 'label.whatsapp_instructions', extensionName: 'ep_theme')}
</div>
</div>
</div>
@@ -176,10 +178,9 @@
</div>
</div>
</div>
<button x-data="scrollTop"
x-bind="trigger"
x-cloak
class="fixed bottom-0 right-0 mb-16 md:mb-8 mr-4 md:mr-8 z-10 h-10 w-10 flex items-center justify-center rounded bg-ep-secondary-80">
<button data-controller="scroll-top"
data-action="scroll-top#scroll"
class="hidden fixed bottom-0 right-0 mb-16 md:mb-8 mr-4 md:mr-8 z-10 h-10 w-10 flex items-center justify-center rounded bg-ep-secondary-80">
<svg class="w-8 h-8 text-white">
<use href="#icon-chevron-up"></use>
</svg>
@@ -1,6 +1,5 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
xmlns:ce="http://typo3.org/ns/TYPO3/CMS/FluidStyledContent/ViewHelpers"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
@@ -9,9 +8,12 @@
<f:if condition="{contentElements}">
<f:for each="{contentElements}" as="contentElement">
<li>
<a href="{f:uri.typolink(parameter: data.pid, section: 'c{contentElement.uid}')}"
class="btn btn-warning ep-sidebar"
@click.prevent="scrollTo('#c{contentElement.uid}')">{contentElement.header}</a>
<button class="button w-full"
data-controller="scroll-to"
data-scroll-to-target-value="c{contentElement.uid}"
data-action="scroll-to#scroll">
{contentElement.header}
</button>
</li>
</f:for>
</f:if>
@@ -0,0 +1,273 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<f:form object="{filterSettings}"
name="filterSettings"
pluginName="Ajax"
data="{'search-target': 'form'}">
<div class="border rounded p-4">
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.dateFrom} || {filterSettings.dateTo}', then: 'false', else: 'true')}">
<label class="cursor-pointer"
data-action="click->collapse#toggle">
Zeitraum
</label>
<div data-collapse-target="container">
<input type="text"
data-search-target="dateRangeLabel"
class="cursor-pointer focus:outline-none"
value="{ep:dateRange(dateFrom: filterSettings.dateFrom, dateTo: filterSettings.dateTo, includeLabel: 0)}"
placeholder="von... bis">
<f:form.hidden property="dateFrom"
data="{'search-target': 'dateFrom'}"/>
<f:form.hidden property="dateTo"
data="{'search-target': 'dateTo'}"/>
<div data-controller="datepicker"
<f:if condition="{filterSettings.dateFrom} && {filterSettings.dateTo}">
data-datepicker-date-from-value="{filterSettings.dateFrom -> f:format.date(format: 'Y-m-d')}"
data-datepicker-date-to-value="{filterSettings.dateTo -> f:format.date(format: 'Y-m-d')}"
</f:if>
data-datepicker-min-date-value="{f:format.date(date: 'now', format: 'Y-m-d')}">
<f:if condition="{settings.datePickerPresets}">
<div class="flex items-center justify-between pb-2">
<f:for each="{settings.datePickerPresets}" as="preset">
<button class="daterange__preset"
data-preset='{preset -> f:format.json()}'
data-action="datepicker#selectPreset">
<span>
{preset.label}
</span>
</button>
</f:for>
</div>
</f:if>
<input type="hidden"
data-datepicker-target="field">
<div class="flex items-center justify-between pt-2">
<button class="daterange__button focus:outline-none">
schließen
</button>
<button class="daterange__button focus:outline-none"
data-action="datepicker#reset">
zurücksetzen
</button>
</div>
</div>
</div>
</div>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.countryUids} || {filterSettings.regionUids}', then: 'false', else: 'true')}">
<label class="cursor-pointer"
data-action="click->collapse#toggle">
Reiseziel
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<f:for each="{filterOptions.destinations}" as="destination">
<li>
<f:form.checkbox class="mr-2"
property="countryUids"
data="{action: 'search#filter'}"
value="{destination.country.uid}"/>
{destination.country.label}
<f:if condition="{destination.regions}">
<ul class="pl-4 pt-1">
<f:for each="{destination.regions}" as="region">
<li>
<f:form.checkbox class="mr-2"
property="regionUids"
data="{action: 'search#filter'}"
value="{region.uid}"/>
{region.label}
</li>
</f:for>
</ul>
</f:if>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.conceptUids}', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Reisekonzepte
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<f:for each="{filterOptions.concepts}" as="concept">
<li>
<f:form.checkbox class="mr-2"
property="conceptUids"
data="{action: 'search#filter'}"
value="{concept.uid}"/>
{concept.name}
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.priceRanges}', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Preis
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<f:for each="{filterOptions.priceRanges}" as="range">
<li>
<label>
<f:form.checkbox class="mr-2"
property="priceRanges"
data="{action: 'search#filter'}"
value="{range.uid}"/>
{ep:priceRange(min: range.min, max: range.max)}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.pax} > 1', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Personen
</label>
<div data-collapse-target="container">
<div class="border rounded">
<select name="pax"
data-action="search#filter">
<f:for each="{filterOptions.pax}" as="option">
<option value="<f:format.raw>{option}</f:format.raw>">
<f:format.raw>{option}</f:format.raw>
</option>
</f:for>
</select>
</div>
</div>
</div>
<f:variable name="hotelTypes" value="{
1: 'Ferienwohnung',
2: 'Sportclub',
3: 'Appartment',
4: 'Gästehaus',
5: 'Pension'
}"/>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.hotelTypes}', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Unterkunft
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<f:for each="{filterOptions.hotelTypes}" as="type">
<li>
<label>
<f:form.checkbox class="mr-2"
property="hotelTypes"
data="{action: 'search#filter'}"
value="{type}"/>
{hotelTypes.{type}}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.boardTypes}', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Verpflegung
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<f:for each="{filterOptions.boardTypes}" as="type">
<f:if condition="{type.label}">
<li>
<label>
<f:form.checkbox class="mr-2"
property="boardTypes"
data="{action: 'search#filter'}"
value="{type.uid}"/>
{type.label}
</label>
</li>
</f:if>
</f:for>
</ul>
</div>
</div>
<f:variable name="roomTypes" value="{
1: 'EZ',
2: 'DZ / 2er',
3: '3er',
4: '4er',
5: 'ab 5 Pers.',
6: 'mit anderen'
}"/>
<div class="mb-2 pb-2 border-b"
data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: '{filterSettings.roomTypes}', then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Zimmerarten/Apart.
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0 p-0">
<f:for each="{filterOptions.roomTypes}" as="type">
<li>
<label>
<f:form.checkbox class="mr-2"
property="roomTypes"
data="{action: 'search#filter'}"
value="{type}"/>
{roomTypes.{type}}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<f:if condition="{filterOptions.busAvailable}">
<div data-controller="collapse"
data-collapse-collapsed-value="{f:if(condition: filterSettings.bus, then: 'false', else: 'true')}">
<label data-action="click->collapse#toggle">
Anreise
</label>
<div data-collapse-target="container">
<ul class="mb-0 p-0">
<li>
<label>
<f:form.checkbox class="mr-2"
property="bus"
data="{action: 'search#filter'}"
value="1"/>
Busanreise
</label>
</li>
</ul>
</div>
</div>
</f:if>
</div>
</f:form>
</html>
@@ -0,0 +1,30 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="grid grid-cols-3 gap-8">
<div class="col-span-3 lg:col-span-1 relative"
data-controller="collapse"
data-collapse-until-value="991">
<p class="lg:hidden">
<button class="button button--small w-full"
data-action="collapse#toggle">
<span>Filter ein-/ausblenden</span>
</button>
</p>
<div class="lg:block"
data-collapse-target="container">
<h3>Filter</h3>
<button class="button button--small w-full"
data-action="search#reset">
Filter zurücksetzen
</button>
<f:render partial="Search/FilterPanel" arguments="{_all}"/>
</div>
</div>
<div class="col-span-3 lg:col-span-2 relative">
<f:render partial="Search/Result" arguments="{_all}"/>
</div>
</div>
</html>
@@ -0,0 +1,144 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="searchresult__info alert alert-info">
Deine Suche
<f:if condition="{meta.organic}">
<f:then>
<f:if condition="{meta.destinationName}">
nach <strong>{meta.destinationName}</strong>
</f:if>
<f:if condition="{meta.dateRange}">
<strong>{meta.dateRange}</strong>
</f:if>
ergab {meta.total} Treffer
</f:then>
<f:else>
ergab leider keine genauen Treffer. Bitte benutze die Filter.
</f:else>
</f:if>
</div>
<div class="searchresult__content">
<f:for each="{searchResult}" as="section">
<div id="concept-{section.concept.uid}">
<h2 class="headline headline--secondary headline--underlined">
{section.concept.name}
</h2>
<div class="searchresult-section searchresult-section--{section.concept.code}">
<f:for each="{section.items}" as="item">
<div class="teaserbox teaserbox--landscape teaserbox--searchresult searchresult-item">
<div class="teaserbox__image">
<img src="{item.country.flag}" class="teaserbox__flag" alt="{item.country.name}"/>
<a href="{item.detailPageUri}" rel="nofollow" title="Details &amp; Buchen">
<picture class="searchresult-item__image">
<source media="(min-width: 1201px)" data-srcset="{item.hotel.images.resized.m.0.url} 260w"/>
<source media="(max-width: 1200px)" data-srcset="{item.hotel.images.resized.l.0.url} 280w"/>
<source media="(max-width: 768px)" data-srcset="{item.hotel.images.resized.s.0.url} 220w"/>
<img class="scale lazyload" data-src="{item.hotel.images.resized.s[0].url}"
title="{item.hotel.images.resized.s.0.title}"
alt="{item.hotel.images.resized.s.0.alt}">
</picture>
</a>
<div class="concept-badge concept-badge--{section.concept.code}"
id="{section.concept.code}">
{section.concept.name}
</div>
</div>
<div class="teaserbox__main">
<div class="teaserbox__header">
<span class="searchresult-item__headline">
{item.region.name}
<f:if condition="{section.concept.code} == 'evt'">
<strong>
<span class="block">
{item.product.name}
</span>
</strong>
</f:if>
</span>
<span class="searchresult-item__subline">
{item.hotel.name}
</span>
<f:if condition="{item.hotel.category}">
<span class="searchresult-item__category">
Kategorie {item.hotel.category}
</span>
</f:if>
</div>
<ul class="check-list searchresult-item__checklist">
<f:if condition="{item.hotel.fact}">
<li>{item.hotel.fact}</li>
</f:if>
<f:if condition="{item.product.fact}">
<li>{item.product.fact}</li>
</f:if>
<f:if condition="{item.region.feature}">
<f:then>
<li>{item.region.feature}</li>
</f:then>
<f:else>
<f:if condition="{item.region.fact}">
<li>{item.region.fact}</li>
</f:if>
</f:else>
</f:if>
</ul>
<ul class="check-list check-list--inverted searchresult-item__featurelist">
<li>Skipass</li>
<f:if condition="{item.board}">
<li>{item.board}</li>
</f:if>
<f:if condition="{item.product.feature}">
<li>{item.product.feature}</li>
</f:if>
</ul>
</div>
<div class="teaserbox__aside">
<div class="teaserbox__aside__top">
<span class="searchresult-item__label">
{f:if(condition: '{item.dates -> f:count()} > 1', then: 'Zeitraum', else: 'Termin')}
</span>
<span class="searchresult-item__daterange">
{ep:dateRange(dateFrom: item.minDateStart, dateTo: item.maxDateEnd, includeLabel: 0)}
</span>
<f:if condition="{item.dates -> f:count()} > 0">
<f:if condition="{item.daytrip}">
<f:else>
<a href="{item.detailPageUri}{f:if(condition: referringPageUri, then: '?ref={referringPageUri -> f:format.urlencode()}')}" rel="nofollow" title="Details &amp; Buchen">
<span class="searchresult-item__daterange">
<i class="fa fa-arrow-circle-right"></i> {item.dates -> f:count()} {f:if(condition: '{item.dates -> f:count()} > 1', then: 'Termine', else: 'Termin')}
</span>
</a>
</f:else>
</f:if>
</f:if>
<hr>
<span class="searchresult-item__label">
{f:if(condition: item.daytrip, then: 'Tageweise buchbar', else: '{ep:nightsOptions(options: item.nights, daytrip: item.daytrip)}')}
</span>
</div>
<div class="teaserbox__aside__bottom">
<span class="searchresult-item__price">ab {item.minPrice} €</span>
<a href="{item.detailPageUri}{f:if(condition: referringPageUri, then: '?ref={referringPageUri -> f:format.urlencode()}')}" class="button button--full searchresult-item__button"
rel="nofollow" title="Details">
Details{f:if(condition: item.hideBookingButton, else: ' &amp; Buchen')}
</a>
</div>
</div>
<button class="teaserbox-watchlist" x-data="watchlistToggle('{item.key}')" x-bind="trigger">
<span data-microtip-position="top" x-bind:aria-label="label" role="tooltip">
<svg class="w-5 h-5">
<use x-bind:href="onList() ? '#icon-minus-circle' : '#icon-plus-circle'"></use>
</svg>
</span>
</button>
</div>
</f:for>
</div>
</div>
</f:for>
</div>
</html>
@@ -1,419 +1,19 @@
<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">
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<f:layout name="Default"/>
<f:section name="Main">
<div class="grid grid-cols-3 gap-8" x-ref="container">
<div class="col-span-3 lg:col-span-1 relative" x-data="collapse(false, 991)">
<p class="lg:hidden">
<button class="button button--full button--small" role="button" x-bind="trigger">
<i class="fa fa-toggle-on"></i> Filter ein-/ausblenden
</button>
</p>
<div class="lg:block"
x-bind="container"
x-transition
x-cloak
>
<h3>Filter</h3>
<button class="button button--sidebar button--small"
x-show="filterCount > 0"
x-cloak
x-on:click.prevent="reset">
Filter zurücksetzen
</button>
<f:render section="FilterPanel" arguments="{_all}"/>
</div>
</div>
<div class="col-span-3 lg:col-span-2 relative">
<f:render section="SearchResult" arguments="{_all}"/>
</div>
</div>
<f:render partial="Search/Main" arguments="{_all}"/>
</f:section>
<f:section name="SearchResult">
<div class="searchresult__info alert alert-info">
Deine Suche
<f:if condition="{meta.organic}">
<f:then>
<f:if condition="{meta.destinationName}">
nach <strong>{meta.destinationName}</strong>
</f:if>
<f:if condition="{meta.dateRange}">
<strong>{meta.dateRange}</strong>
</f:if>
ergab {meta.total} Treffer
</f:then>
<f:else>
ergab leider keine genauen Treffer. Bitte benutze die Filter.
</f:else>
</f:if>
</div>
<div class="searchresult__content">
<f:for each="{searchResult}" as="section">
<div id="concept-{section.concept.uid}">
<h2 class="headline headline--secondary headline--underlined">
{section.concept.name}
</h2>
<div class="searchresult-section searchresult-section--{section.concept.code}">
<f:for each="{section.items}" as="item">
<div class="teaserbox teaserbox--landscape teaserbox--searchresult searchresult-item">
<div class="teaserbox__image">
<img src="{item.country.flag}" class="teaserbox__flag" alt="{item.country.name}"/>
<a href="{item.detailPageUri}" rel="nofollow" title="Details &amp; Buchen">
<picture class="searchresult-item__image">
<source media="(min-width: 1201px)" data-srcset="{item.hotel.images.resized.m.0.url} 260w"/>
<source media="(max-width: 1200px)" data-srcset="{item.hotel.images.resized.l.0.url} 280w"/>
<source media="(max-width: 768px)" data-srcset="{item.hotel.images.resized.s.0.url} 220w"/>
<img class="scale lazyload" data-src="{item.hotel.images.resized.s[0].url}"
title="{item.hotel.images.resized.s.0.title}"
alt="{item.hotel.images.resized.s.0.alt}">
</picture>
</a>
<div class="concept-badge concept-badge--{section.concept.code}"
id="{section.concept.code}">
{section.concept.name}
</div>
</div>
<div class="teaserbox__main">
<div class="teaserbox__header">
<span class="searchresult-item__headline">
{item.region.name}
<f:if condition="{section.concept.code} == 'evt'">
<strong>
<span class="block">
{item.product.name}
</span>
</strong>
</f:if>
</span>
<span class="searchresult-item__subline">
{item.hotel.name}
</span>
<f:if condition="{item.hotel.category}">
<span class="searchresult-item__category">
Kategorie {item.hotel.category}
</span>
</f:if>
</div>
<ul class="check-list searchresult-item__checklist">
<f:if condition="{item.hotel.fact}">
<li>{item.hotel.fact}</li>
</f:if>
<f:if condition="{item.product.fact}">
<li>{item.product.fact}</li>
</f:if>
<f:if condition="{item.region.feature}">
<f:then>
<li>{item.region.feature}</li>
</f:then>
<f:else>
<f:if condition="{item.region.fact}">
<li>{item.region.fact}</li>
</f:if>
</f:else>
</f:if>
</ul>
<ul class="check-list check-list--inverted searchresult-item__featurelist">
<li>Skipass</li>
<f:if condition="{item.board}">
<li>{item.board}</li>
</f:if>
<f:if condition="{item.product.feature}">
<li>{item.product.feature}</li>
</f:if>
</ul>
</div>
<div class="teaserbox__aside">
<div class="teaserbox__aside__top">
<span class="searchresult-item__label">
{f:if(condition: '{item.dates -> f:count()} > 1', then: 'Zeitraum', else: 'Termin')}
</span>
<span class="searchresult-item__daterange">
{ep:dateRange(dateFrom: item.minDateStart, dateTo: item.maxDateEnd, includeLabel: 0)}
</span>
<f:if condition="{item.dates -> f:count()} > 0">
<f:if condition="{item.daytrip}">
<f:else>
<a href="{item.detailPageUri}{f:if(condition: referringPageUri, then: '?ref={referringPageUri -> f:format.urlencode()}')}" rel="nofollow" title="Details &amp; Buchen">
<span class="searchresult-item__daterange">
<i class="fa fa-arrow-circle-right"></i> {item.dates -> f:count()} {f:if(condition: '{item.dates -> f:count()} > 1', then: 'Termine', else: 'Termin')}
</span>
</a>
</f:else>
</f:if>
</f:if>
<hr>
<span class="searchresult-item__label">
{f:if(condition: item.daytrip, then: 'Tageweise buchbar', else: '{ep:nightsOptions(options: item.nights, daytrip: item.daytrip)}')}
</span>
</div>
<div class="teaserbox__aside__bottom">
<span class="searchresult-item__price">ab {item.minPrice} €</span>
<a href="{item.detailPageUri}{f:if(condition: referringPageUri, then: '?ref={referringPageUri -> f:format.urlencode()}')}" class="button button--full searchresult-item__button"
rel="nofollow" title="Details">
Details{f:if(condition: item.hideBookingButton, else: ' &amp; Buchen')}
</a>
</div>
</div>
<button class="teaserbox-watchlist" x-data="watchlistToggle('{item.key}')" x-bind="trigger">
<span data-microtip-position="top" x-bind:aria-label="label" role="tooltip">
<svg class="w-5 h-5">
<use x-bind:href="onList() ? '#icon-minus-circle' : '#icon-plus-circle'"></use>
</svg>
</span>
</button>
</div>
</f:for>
</div>
</div>
</f:for>
</div>
<f:render partial="Search/Result" arguments="{_all}"/>
</f:section>
<f:section name="FilterPanel">
<div class="border rounded p-4">
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.dateFrom} || {filterSettings.dateTo}">false</f:if>)"
x-init='initDatePicker(
<f:format.raw>
"{filterOptions.dateFrom -> f:format.date(format: 'Y-m-d')}",
"{filterOptions.dateTo -> f:format.date(format: 'Y-m-d')}"
</f:format.raw>
)'>
<label x-bind="trigger">
Zeitraum
</label>
<div x-bind="container" class="relative">
<input type="text"
class="cursor-pointer focus:outline-none"
placeholder="von... bis"
x-bind:value="dateRangeLabel"
x-bind:class="loading && 'cursor-not-allowed'"
x-on:click.prevent="show = show === 'dateRange' ? null : 'dateRange'"/>
<div class="absolute top-100 left-0 z-20 bg-white p-4 shadow-md" x-show="show === 'dateRange'" x-transition x-cloak>
<template x-if="datePresets">
<div class="flex items-center justify-between pb-2">
<template x-for="preset in datePresets">
<a class="daterange__preset" href="#"
x-on:click.prevent="selectDateRangePreset(preset)">
<i class="fa fa-calendar"></i> <span x-text="preset.label"></span>
</a>
</template>
</div>
</template>
<input x-ref="picker" type="hidden">
<div class="flex items-center justify-between pt-2">
<button class="daterange__button focus:outline-none" x-on:click.prevent="show = null">
<i class="fa fa-times"></i> schließen
</button>
<button class="daterange__button focus:outline-none" x-on:click.prevent="resetDateRange()">
<i class="fa fa-trash-o"></i> zurücksetzen
</button>
</div>
</div>
</div>
</div>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.countryUids -> f:count()} || {filterSettings.regionUids -> f:count()}">false</f:if>)">
<label x-bind="trigger" class="cursor-pointer">
Reiseziel
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<f:for each="{filterOptions.destinations}" as="destination">
<li>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.countryUids"
value="{destination.country.uid}">
{destination.country.label}
<f:if condition="{destination.regions -> f:count()}">
<ul>
<f:for each="{destination.regions}" as="region">
<li>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.regionUids"
value="{region.uid}">
{region.label}
</li>
</f:for>
</ul>
</f:if>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.conceptUids -> f:count()}">false</f:if>)">
<label x-bind="trigger">
Reisekonzepte
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<f:for each="{filterOptions.concepts}" as="concept">
<li>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.conceptUids"
value="{concept.uid}">
{concept.name}
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.priceRanges -> f:count()}">false</f:if>)">
<label x-bind="trigger">
Preis
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<f:for each="{filterOptions.priceRanges}" as="range">
<li>
<label>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.priceRanges"
value="<f:format.raw>{range.uid}</f:format.raw>">
{ep:priceRange(min: range.min, max: range.max)}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.pax} > 1">false</f:if>)">
<label x-bind="trigger">
Personen
</label>
<div x-bind="container" x-transition>
<div class="border rounded">
<select x-model="filterSettings.pax" x-on:change="refresh">
<f:for each="{filterOptions.pax}" as="option">
<option value="<f:format.raw>{option}</f:format.raw>">
<f:format.raw>{option}</f:format.raw>
</option>
</f:for>
</select>
</div>
</div>
</div>
<f:variable name="hotelTypes" value="{
1: 'Ferienwohnung',
2: 'Sportclub',
3: 'Appartment',
4: 'Gästehaus',
5: 'Pension'
}"/>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.hotelTypes -> f:count()}">false</f:if>)">
<label x-bind="trigger">
Unterkunft
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<f:for each="{filterOptions.hotelTypes}" as="type">
<li>
<label>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.hotelTypes"
value="<f:format.raw>{type}</f:format.raw>">
{hotelTypes.{type}}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.boardTypes -> f:count()}">false</f:if>)">
<label x-bind="trigger">
Verpflegung
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<f:for each="{filterOptions.boardTypes}" as="type">
<f:if condition="{type.label}">
<li>
<label>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.boardTypes"
value="<f:format.raw>{type.uid}</f:format.raw>">
{type.label}
</label>
</li>
</f:if>
</f:for>
</ul>
</div>
</div>
<f:variable name="roomTypes" value="{
1: 'EZ',
2: 'DZ / 2er',
3: '3er',
4: '4er',
5: 'ab 5 Pers.',
6: 'mit anderen'
}"/>
<div class="mb-2 pb-2 border-b" x-data="collapse(<f:if condition="{filterSettings.roomTypes -> f:count()}">false</f:if>)">
<label x-bind="trigger">
Zimmerarten/Apart.
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0 p-0">
<f:for each="{filterOptions.roomTypes}" as="type">
<li>
<label>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model.number="filterSettings.roomTypes"
value="<f:format.raw>{type}</f:format.raw>">
{roomTypes.{type}}
</label>
</li>
</f:for>
</ul>
</div>
</div>
<f:if condition="{filterOptions.busAvailable}">
<div x-data="collapse(<f:if condition="{filterSettings.bus}">false</f:if>)">
<label x-bind="trigger">
Anreise
</label>
<div x-bind="container" x-transition>
<ul class="mb-0 p-0">
<li>
<label>
<input type="checkbox"
class="mr-2"
x-on:change="refresh"
x-model="filterSettings.bus">
Busanreise
</label>
</li>
</ul>
</div>
</div>
</f:if>
</div>
<f:render partial="Search/FilterPanel" arguments="{_all}"/>
</f:section>
</html>
@@ -11,11 +11,12 @@
<div class="container">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<div class="mb-8 lg:hidden">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</div>
</f:if>
<f:render partial="Header/Header" arguments="{layout: 8, subheader: city.name, header: city.headline}"/>
@@ -26,11 +27,12 @@
<div class="col-span-3 lg:col-span-1">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<p class="hidden lg:block">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</p>
</f:if>
<f:render partial="Facts" arguments="{facts: city.facts}"/>
@@ -11,11 +11,12 @@
<div class="container">
<f:if condition="{teasers}">
<div class="mb-8 lg:hidden">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</div>
</f:if>
<f:render partial="Header/Header" arguments="{layout: 8, subheader: concept.name, header: concept.headline}"/>
@@ -50,11 +51,12 @@
<div class="col-span-3 lg:col-span-1">
<f:if condition="{teasers}">
<p class="hidden lg:block">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</p>
</f:if>
<v:menu pageUid="{settings.conceptsPageUid}" levels="1">
@@ -11,11 +11,12 @@
<div class="container">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<div class="mb-8 lg:hidden">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</div>
</f:if>
<f:render partial="Header/Header" arguments="{layout: 8, subheader: country.name, header: country.headline}"/>
@@ -53,11 +54,12 @@
<div class="col-span-3 lg:col-span-1">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<p class="hidden lg:block">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</p>
</f:if>
<f:render partial="Facts" arguments="{facts: country.facts}"/>
@@ -13,11 +13,11 @@
<f:variable name="latitude" value="{region.0.data.latitude}"/>
<f:variable name="longitude" value="{region.0.data.longitude}"/>
</f:if>
<div class="osm-map"
x-data="osmMap"
data-lat="{latitude}"
data-lng="{longitude}"
data-zoom="17"></div>
<div class="w-full aspect-w-16 aspect-h-9 mb-8"
data-controller="osm"
data-osm-lat-value="{latitude}"
data-osm-lng-value="{longitude}"
data-osm-zoom-value="17"></div>
</f:section>
</html>
@@ -10,11 +10,12 @@
<f:if condition="{page.content}">
<f:for each="{page.content}" as="element">
<f:if condition="{element.data.header}">
<a href="{page.link}#c{element.data.uid}"
class="button w-full"
x-data="scrollTo('c{element.data.uid}')">
<button class="button w-full"
data-controller="scroll-to"
data-scroll-to-target-value="c{element.data.uid}"
data-action="scroll-to#scroll">
{element.data.header}
</a>
</button>
</f:if>
</f:for>
</f:if>
@@ -10,11 +10,12 @@
<div class="container">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<div class="mb-8 lg:hidden">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</div>
</f:if>
<f:render partial="Header/Header" arguments="{layout: 8, subheader: hotel.name, header: hotel.headline}"/>
@@ -25,11 +26,12 @@
<div class="col-span-3 md:col-span-1">
<f:if condition="{v:content.get(column: 1) -> f:count()} > 0">
<p class="hidden lg:block">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</p>
</f:if>
<f:render partial="Facts" arguments="{facts: hotel.facts}"/>
@@ -40,14 +42,11 @@
<div class="bg-gray-200 p-4">
<p><strong>Lage</strong></p>
<div>
<f:format.raw>
<div class="osm-map"
x-data="osmMap"
data-lat="{hotel.latitude}"
data-lng="{hotel.longitude}"
data-zoom="12"
data-link="{f:uri.typolink(parameter: hotel.externalLink)}"></div>
</f:format.raw>
<div class="w-full aspect-w-16 aspect-h-9"
data-controller="osm"
data-osm-lat-value="{hotel.latitude}"
data-osm-lng-value="{hotel.longitude}"
data-osm-external-link-value="{f:uri.typolink(parameter: hotel.externalLink)}"></div>
</div>
</div>
</div>
@@ -17,9 +17,10 @@
<f:if condition="{product.bookable}">
<h2>Buchungsoptionen</h2>
<div class="ajax-content-wrapper"
x-data="ajaxContent('{ep:uri.ajax(action: 'pricetableHtml', controller: 'AjaxTable', pageUid: settings.defaultAjaxUid, arguments: '{product: product, hotel: hotel}', format: 'html')}')">
<div class="ajax-content" x-ref="container"></div>
<div x-ref="trigger">
data-controller="ajax-content"
data-ajax-content-uri-value="{ep:uri.ajax(action: 'pricetableHtml', controller: 'AjaxTable', pageUid: settings.defaultAjaxUid, arguments: '{product: product, hotel: hotel}', format: 'html')}">
<div class="ajax-content" data-ajax-content-target="container"></div>
<div data-ajax-content-target="trigger">
<img src="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif')}" alt="Loading" title="Loading">
</div>
</div>
@@ -7,8 +7,8 @@
<f:section name="Main">
<ep:container data="{data}">
<div class="w-full aspect-w-16 aspect-h-9 mb-8"
x-data="osmMap"
data-markers='{markerData -> f:format.raw()}'></div>
data-controller="osm"
data-osm-markers-value='{markerData -> f:format.raw()}'></div>
</ep:container>
</f:section>
@@ -8,9 +8,10 @@
<f:section name="Main">
<div class="ajax-content-wrapper"
x-data="ajaxContent('{ep:uri.ajax(extensionName: 'epproducts', action: 'eventPricetable', controller: 'AjaxTable', pageUid: settings.defaultAjaxUid, arguments: '{product: productUid}', format: 'html')}')">
<div class="ajax-content" x-ref="container"></div>
<div x-ref="trigger">
data-controller="ajax-content"
data-ajax-content-uri-value="{ep:uri.ajax(extensionName: 'epproducts', action: 'eventPricetable', controller: 'AjaxTable', pageUid: settings.defaultAjaxUid, arguments: '{product: productUid}', format: 'html')}">
<div class="ajax-content" data-ajax-content-target="container"></div>
<div data-ajax-content-target="trigger">
<img src="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif')}" alt="Loading" title="Loading">
</div>
</div>
@@ -60,7 +60,11 @@
</f:if>
<f:if condition="{product.faqs}">
<button class="button button--light col-span-1 md:col-span-2"
x-data="scrollTo('faq')">FAQ</button>
data-controller="scroll-to"
data-scroll-to-target-value="faq"
data-action="scroll-to#scroll">
FAQ
</button>
</f:if>
<div class="col-span-2">
<p class="mb-4">
@@ -184,13 +188,12 @@
<p class="font-bold">
Lage
</p>
<div>
<div class="osm-map"
x-data="osmMap"
data-lat="{hotel.latitude}"
data-lng="{hotel.longitude}"
data-link="{f:uri.typolink(parameter: hotel.externalLink)}"></div>
</div>
<div class="w-full aspect-w-16 aspect-h-9"
data-controller="osm"
data-osm-zoom-value="15"
data-osm-lat-value="{hotel.latitude}"
data-osm-lng-value="{hotel.longitude}"
data-osm-external-link-value="{f:uri.typolink(parameter: hotel.externalLink)}"></div>
</div>
<f:render partial="Region/Webcam" arguments="{region: product.region}"/>
<f:render partial="RegionMaps" arguments="{region: product.region}"/>
@@ -11,11 +11,12 @@
<div class="container">
<f:if condition="{teasers}">
<div class="mb-8 lg:hidden">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</div>
</f:if>
<f:render partial="Header/Header" arguments="{layout: 8, subheader: region.name, header: region.headline}"/>
@@ -26,11 +27,12 @@
<div class="col-span-3 lg:col-span-1">
<f:if condition="{teasers}">
<p class="hidden lg:block">
<a href="#teasers"
x-data="scrollTo('teasers')"
class="button w-full mb-8">
<button class="button w-full mb-8"
data-controller="scroll-to"
data-scroll-to-target-value="teasers"
data-action="scroll-to#scroll">
Direkt zu den Angeboten
</a>
</button>
</p>
</f:if>
<f:if condition="{region.winter}">
@@ -44,7 +46,8 @@
</div>
</div>
</div>
<div x-data='fbpixel({ "content_ids": [ {region.uid -> f:format.raw()} ], "content_name": "{region.nameInternal -> f:format.raw()}", "content_type": "product" })'></div>
<div data-controller="fbpixel"
data-fbpixel-props-value='{ "content_ids": [ {region.uid -> f:format.raw()} ], "content_name": "{region.nameInternal -> f:format.raw()}", "content_type": "product" }'></div>
</f:section>
</html>
@@ -35,15 +35,16 @@
</f:section>
<f:section name="CollapsibleSimple">
<div class="panel panel-default" x-data="collapse">
<div class="panel panel-default"
data-controller="collapse">
<div class="panel-heading">
<h4 class="panel-title">
<a role="button" href="#" x-bind="trigger">
<button data-action="collapse#toggle">
{label}
</a>
</button>
</h4>
</div>
<div class="hidden" x-bind="container">
<div class="hidden" data-collapse-target="container">
<div class="panel-body">
<f:if condition="{name}">
<div class="form-group">
@@ -69,15 +70,17 @@
</f:section>
<f:section name="CollapsibleHotel">
<div class="panel panel-default" x-data="collapse">
<div class="panel panel-default"
data-controller="collapse">
<div class="panel-heading">
<h4 class="panel-title">
<a role="button" href="#" data-bind="trigger">
<button data-action="collapse#toggle">
{label}
</a>
</button>
</h4>
</div>
<div class="hidden" data-bind="container">
<div class="hidden"
data-collapse-target="container">
<div class="panel-body">
<div class="form-group">
<label class="control-label">Name</label>
@@ -229,15 +232,17 @@
</f:section>
<f:section name="CollapsibleCity">
<div class="panel panel-default" x-data="collapse">
<div class="panel panel-default"
data-controller="collapse">
<div class="panel-heading">
<h4 class="panel-title">
<a role="button" href="#" x-bind="trigger">
<button data-action="collapse#toggle">
{label}
</a>
</button>
</h4>
</div>
<div class="hidden" x-bind="container">
<div class="hidden"
data-collapse-target="container">
<div class="panel-body">
<div class="form-group">
<label class="control-label">Name</label>
@@ -257,15 +262,17 @@
</f:section>
<f:section name="CollapsibleRegion">
<div class="panel panel-default" x-data="collapse">
<div class="panel panel-default"
data-controller="collapse">
<div class="panel-heading">
<h4 class="panel-title">
<a role="button" href="#" x-bind="trigger">
<button data-action="collapse#toggle">
{label}
</a>
</button>
</h4>
</div>
<div class="hidden" x-bind="container">
<div class="hidden"
data-collapse-target="container">
<div class="panel-body">
<div class="form-group">
<label class="control-label">Name</label>
@@ -309,15 +316,17 @@
</f:section>
<f:section name="CollapsibleImages">
<div class="panel panel-default" x-data="collapse">
<div class="panel panel-default"
data-controller="collapse">
<div class="panel-heading">
<h4 class="panel-title">
<a role="button" href="#" x-bind="trigger">
<button data-action="collapse#toggle">
{label}
</a>
</button>
</h4>
</div>
<div class="hidden" x-bind="container">
<div class="hidden"
data-collapse-target="container">
<div class="panel-body">
<f:for each="{images}" as="image">
<a href="{image}" target="_blank">{image}</a><br>
@@ -14,7 +14,7 @@
sticky: <f:if condition="{fixed}"><f:then>true</f:then><f:else>false</f:else></f:if>
</f:format.raw>
})'>
<div class="searchbar {f:if(condition: fixed, then: ' searchbar--sticky')}"{f:if(condition: fixed, else: ' x-data="sticky(650)" x-bind:class="sticky && \'searchbar--sticky\'"')}>
<div class="searchbar {f:if(condition: fixed, then: ' searchbar--sticky')}"{f:if(condition: fixed, else: ' data-controller="sticky" data-sticky-offset-value="650" data-sticky-sticky-class="searchbar--sticky"')}>
<f:form action="processSearch" controller="Search" method="post" name="searchParams"
object="{searchParams}" pluginName="searchresult" extensionName="epproducts"
pageUid="{settings.defaultSearchPageUid}" additionalAttributes="{ref: 'form'}">
@@ -1,7 +1,6 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers">
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers">
<f:layout name="Default"/>
@@ -13,15 +12,14 @@
style="background-image: url({f:uri.image(src: settings.searchPageHeaderImage, width: '1366', height: '160c+100')})"></div>
</div>
<div class="container relative"
x-data='search({
<f:format.raw>
uri: "{ep:uri.ajax(controller: 'AjaxSearch', action: 'searchresult', pageUid: settings.defaultAjaxUid, format: 'html')}",
initialFilterSettings: {filterSettings -> f:format.json()},
datePresets: {settings.datePickerPresets -> f:format.json()}
</f:format.raw>
})'>
<div x-ref="container"></div>
<div x-show="loading" class="absolute z-20 inset-0 w-full h-fill bg-overlay-white">
data-controller="search"
data-search-uri-value="{ep:uri.ajax(controller: 'AjaxSearch', action: 'searchresult', pageUid: settings.defaultAjaxUid, format: 'html')}"
data-action="datepicker:range-select->search#selectDateRange">
<div data-search-target="resultsContainer">
<f:render partial="Search/Main" arguments="{_all}"/>
</div>
<div class="absolute z-20 inset-0 w-full h-fill bg-overlay-white hidden"
data-search-target="loadingIndicator">
<div class="absolute mt-16 top-0 left-1/2 transform -translate-x-1/2 flex items-center space-x-4 alert alert-info">
<f:image src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt="Loading" />
<span>Suchergebnis wird geladen...</span>
+1
View File
@@ -77,6 +77,7 @@ module.exports = {
'primary': '#82C3C6',
'secondary': '#f9c33d',
'primary-10': tinycolor('#82C3C6').setAlpha(0.1).toRgbString(),
'primary-50': tinycolor('#82C3C6').setAlpha(0.5).toRgbString(),
'primary-80': tinycolor('#82C3C6').setAlpha(0.8).toRgbString(),
},
'uch': {