Compare commits
19
Commits
f005272242
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ed5c6962 | ||
|
|
942724fdd1 | ||
|
|
e52f7dcff8 | ||
|
|
30e531497d | ||
|
|
2cb37e760a | ||
|
|
3ff3251419 | ||
|
|
abcd39107b | ||
|
|
599e3c7ee2 | ||
|
|
6ff308015a | ||
|
|
6e0479bf22 | ||
|
|
fb8fddf669 | ||
|
|
f745c7b683 | ||
|
|
9b3550ec40 | ||
|
|
d9943619f6 | ||
|
|
e33b57c9f1 | ||
|
|
dc594efee1 | ||
|
|
432d8da350 | ||
|
|
5b338eeab6 | ||
|
|
05ceae5100 |
@@ -4,8 +4,8 @@
|
|||||||
# http://download.redis.io/redis-stable/redis.conf
|
# http://download.redis.io/redis-stable/redis.conf
|
||||||
# http://download.redis.io/redis-stable/sentinel.conf
|
# http://download.redis.io/redis-stable/sentinel.conf
|
||||||
|
|
||||||
maxmemory 2048mb
|
maxmemory 128mb
|
||||||
maxmemory-policy allkeys-lfu
|
maxmemory-policy allkeys-lru
|
||||||
|
|
||||||
# to disable Redis persistence, remove ddev-generated from this file,
|
# to disable Redis persistence, remove ddev-generated from this file,
|
||||||
# and uncomment the two lines below:
|
# and uncomment the two lines below:
|
||||||
|
|||||||
-3284
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
|
"ext-redis": "*",
|
||||||
"ext-simplexml": "*",
|
"ext-simplexml": "*",
|
||||||
"b13/container": "^1.3",
|
"b13/container": "^1.3",
|
||||||
"blueways/bw-captcha": "^3.1",
|
"blueways/bw-captcha": "^3.1",
|
||||||
|
|||||||
Generated
+3
-2
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "e3a433b77a2142f116e57b54c4172ffa",
|
"content-hash": "38fb94ebf504e8413d37c94f929e4b47",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "b13/container",
|
"name": "b13/container",
|
||||||
@@ -9647,11 +9647,12 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
|
"ext-redis": "*",
|
||||||
"ext-simplexml": "*"
|
"ext-simplexml": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "7.4.13"
|
"php": "7.4.33"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-12
@@ -63,41 +63,39 @@ host('prod')
|
|||||||
->setForwardAgent(true)
|
->setForwardAgent(true)
|
||||||
->setSshMultiplexing(true)
|
->setSshMultiplexing(true)
|
||||||
->setDeployPath('/usr/home/eptypo3/public_html/{{application}}')
|
->setDeployPath('/usr/home/eptypo3/public_html/{{application}}')
|
||||||
->set('buspronet_path', '/usr/home/eptypo3/public_html/kuschick/buspronet/buspronet')
|
|
||||||
->set('bin/php', '/usr/bin/php74')
|
->set('bin/php', '/usr/bin/php74')
|
||||||
->set('http_user', 'eptypo3')
|
->set('http_user', 'eptypo3')
|
||||||
->set('writable_mode', 'chmod')
|
->set('writable_mode', 'chmod')
|
||||||
->set('bin/composer', '/usr/bin/composer')
|
|
||||||
->set('composer_options', '--prefer-dist --no-progress --no-interaction --optimize-autoloader')
|
|
||||||
->set('rsync_src', __DIR__)
|
->set('rsync_src', __DIR__)
|
||||||
->set('rsync', $rsyncOptions)
|
->set('rsync', $rsyncOptions)
|
||||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://www.ep-reisen.de')
|
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/{{typo3_webroot}}/ --web-url=https://www.ep-reisen.de')
|
||||||
;
|
;
|
||||||
|
|
||||||
task('deploy', [
|
task('deploy', [
|
||||||
'deploy:info',
|
'deploy:info',
|
||||||
|
// Build locally first, so a failing build aborts before the server is touched at all.
|
||||||
|
'deploy:assets:build',
|
||||||
'deploy:setup',
|
'deploy:setup',
|
||||||
'deploy:lock',
|
'deploy:lock',
|
||||||
'deploy:release',
|
'deploy:release',
|
||||||
'deploy:assets:build',
|
|
||||||
'rsync',
|
'rsync',
|
||||||
'deploy:shared',
|
'deploy:shared',
|
||||||
'deploy:writable',
|
'deploy:writable',
|
||||||
|
// The schema has to exist before the new code starts serving traffic.
|
||||||
'typo3:database:migrate',
|
'typo3:database:migrate',
|
||||||
'typo3:language:update',
|
'typo3:language:update',
|
||||||
|
// deploy:publish == deploy:symlink, deploy:unlock, deploy:cleanup, deploy:success
|
||||||
'deploy:publish',
|
'deploy:publish',
|
||||||
'cachetool:clear:opcache',
|
|
||||||
'typo3:cache:flush',
|
|
||||||
'deploy:buspronet:symlink',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
task('deploy:buspronet:symlink', function () {
|
// Caches belong to the release that goes live, so they are cleared after the symlink flip. Hooking
|
||||||
run('ln -s {{buspronet_path}} {{release_or_current_path}}/public/buchung');
|
// into deploy:symlink rather than appending to the list above keeps them inside deploy:publish, ahead
|
||||||
});
|
// of deploy:unlock and deploy:success - a failing cache task must not follow a declared success.
|
||||||
|
after('deploy:symlink', 'cachetool:clear:opcache');
|
||||||
|
after('deploy:symlink', 'typo3:cache:flush');
|
||||||
|
|
||||||
task ('deploy:assets:build', function () {
|
task ('deploy:assets:build', function () {
|
||||||
runLocally('ddev exec npm ci');
|
runLocally('ddev exec npm ci');
|
||||||
runLocally('ddev exec npm rebuild node-sass');
|
|
||||||
runLocally('ddev exec npm run build');
|
runLocally('ddev exec npm run build');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
-18
@@ -21,10 +21,8 @@
|
|||||||
"glightbox": "^3.0.9",
|
"glightbox": "^3.0.9",
|
||||||
"htmx.org": "^1.9.12",
|
"htmx.org": "^1.9.12",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"lazysizes": "^5.3.2",
|
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"picturefill": "^3.0.3",
|
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"sticky-js": "^1.3.0",
|
"sticky-js": "^1.3.0",
|
||||||
"stimulus-use": "^0.52.0",
|
"stimulus-use": "^0.52.0",
|
||||||
@@ -6017,12 +6015,6 @@
|
|||||||
"shell-quote": "^1.8.4"
|
"shell-quote": "^1.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lazysizes": {
|
|
||||||
"version": "5.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz",
|
|
||||||
"integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/leaflet": {
|
"node_modules/leaflet": {
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
@@ -6963,16 +6955,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picturefill": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/picturefill/-/picturefill-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-JDdx+3i4fs2pkqwWZJgGEM2vFWsq+01YsQFT9CKPGuv2Q0xSdrQZoxi9XwyNARTgxiOdgoAwWQRluLRe/JQX2g==",
|
|
||||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pify": {
|
"node_modules/pify": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||||
|
|||||||
@@ -24,10 +24,8 @@
|
|||||||
"glightbox": "^3.0.9",
|
"glightbox": "^3.0.9",
|
||||||
"htmx.org": "^1.9.12",
|
"htmx.org": "^1.9.12",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"lazysizes": "^5.3.2",
|
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"picturefill": "^3.0.3",
|
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"sticky-js": "^1.3.0",
|
"sticky-js": "^1.3.0",
|
||||||
"stimulus-use": "^0.52.0",
|
"stimulus-use": "^0.52.0",
|
||||||
|
|||||||
@@ -47,24 +47,49 @@ if (getenv('IS_DDEV_PROJECT') == 'true') {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cache_pages'] = [
|
// Fail fast instead of blocking indefinitely when redis is unreachable.
|
||||||
|
$redisOptions['connectionTimeout'] = 2;
|
||||||
|
|
||||||
|
// Note: the cache identifiers must stay unprefixed ('pages', not 'cache_pages'). These
|
||||||
|
// assignments replace the definitions from LocalConfiguration.php wholesale, so 'groups'
|
||||||
|
// and the compression/lifetime options have to be repeated here.
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
|
||||||
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
||||||
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
||||||
'options' => $redisOptions + ['database' => 0],
|
'options' => $redisOptions + [
|
||||||
|
'database' => 0,
|
||||||
|
'compression' => true,
|
||||||
|
],
|
||||||
|
'groups' => ['pages'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cache_hash'] = [
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['hash'] = [
|
||||||
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
||||||
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
||||||
'options' => $redisOptions + ['database' => 1],
|
'options' => $redisOptions + [
|
||||||
|
'database' => 1,
|
||||||
|
],
|
||||||
|
'groups' => ['pages'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cache_rootline'] = [
|
// Own database: RedisBackend::flush() issues FLUSHDB and the keys carry no per-cache
|
||||||
|
// prefix, so two caches sharing a database flush each other.
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline'] = [
|
||||||
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
|
||||||
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
|
||||||
'options' => $redisOptions + ['database' => 1],
|
'options' => $redisOptions + [
|
||||||
|
'database' => 2,
|
||||||
|
'compression' => true,
|
||||||
|
'defaultLifetime' => 2592000,
|
||||||
|
],
|
||||||
|
'groups' => ['pages'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Assets are served through the bunny.net CDN, which gzip/brotli-compresses at the edge.
|
||||||
|
// Pre-gzipped .gzip twins are served with Content-Encoding: gzip and cannot be processed
|
||||||
|
// by the CDN, so stop generating them. mod_deflate still compresses direct origin hits.
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 0;
|
||||||
|
|
||||||
// suppress php deprecation warnings flooding typo3 logs
|
// suppress php deprecation warnings flooding typo3 logs
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] =
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] =
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] & ~E_DEPRECATED & ~E_USER_DEPRECATED;
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] & ~E_DEPRECATED & ~E_USER_DEPRECATED;
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import Vue from 'vue'
|
|||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import VueSession from 'vue-session'
|
import VueSession from 'vue-session'
|
||||||
|
|
||||||
import 'lazysizes';
|
|
||||||
import 'lazysizes/plugins/attrchange/ls.attrchange';
|
|
||||||
|
|
||||||
const slick = require('slick-carousel');
|
const slick = require('slick-carousel');
|
||||||
const Sticky = require('sticky-js');
|
const Sticky = require('sticky-js');
|
||||||
const $window = $(window);
|
const $window = $(window);
|
||||||
|
|||||||
+3
-3
@@ -38,9 +38,9 @@
|
|||||||
<div class="card card--no-form offerlist-item__card">
|
<div class="card card--no-form offerlist-item__card">
|
||||||
<div class="card__inner">
|
<div class="card__inner">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(max-width: 512px)" :data-srcset="offer.teaserImageMobile + ' 500w'"/>
|
<source media="(max-width: 512px)" :srcset="offer.teaserImageMobile"/>
|
||||||
<img class="card__image scale lazyload" data-sizes="auto"
|
<img class="card__image scale"
|
||||||
:data-src="offer.teaserImage" alt="">
|
:src="offer.teaserImage" alt="" loading="lazy">
|
||||||
</picture>
|
</picture>
|
||||||
<div class="card__caption">
|
<div class="card__caption">
|
||||||
<span class="card__subtitle" v-html="offer.name"></span>
|
<span class="card__subtitle" v-html="offer.name"></span>
|
||||||
|
|||||||
@@ -87,12 +87,16 @@ class SearchController extends ActionController
|
|||||||
|
|
||||||
public function initializeSearchresultAction()
|
public function initializeSearchresultAction()
|
||||||
{
|
{
|
||||||
// Process encoded filtersettings from static search result
|
// Process encoded filtersettings from static search result.
|
||||||
|
// Explicit filter settings win: they carry the visitor's current selection,
|
||||||
|
// whereas a stale 'f' riding along on a link would silently replace it.
|
||||||
$encodedFilterSettings = GeneralUtility::_GET('f');
|
$encodedFilterSettings = GeneralUtility::_GET('f');
|
||||||
if ($encodedFilterSettings) {
|
if ($encodedFilterSettings && !$this->request->hasArgument('filterSettings')) {
|
||||||
$filterSettingsEncoder = new FilterSettingsEncoder();
|
$filterSettingsEncoder = new FilterSettingsEncoder();
|
||||||
$filterSettings = $filterSettingsEncoder->decode($encodedFilterSettings);
|
$filterSettings = $filterSettingsEncoder->decode($encodedFilterSettings);
|
||||||
$this->request->setArgument('filterSettings', $filterSettings);
|
if (count($filterSettings) > 0) {
|
||||||
|
$this->request->setArgument('filterSettings', $filterSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'], true);
|
$forcedConceptUids = GeneralUtility::intExplode(',', $this->settings['forcedConceptUids'], true);
|
||||||
$this->processFilterSettingsArgument($forcedConceptUids);
|
$this->processFilterSettingsArgument($forcedConceptUids);
|
||||||
|
|||||||
@@ -30,12 +30,17 @@ namespace EP\EpProducts\Domain\Model;
|
|||||||
class Room
|
class Room
|
||||||
{
|
{
|
||||||
|
|
||||||
|
const ROOM_TYPE_UNCLASSIFIED = 0;
|
||||||
const ROOM_TYPE_FOR_ONE = 1;
|
const ROOM_TYPE_FOR_ONE = 1;
|
||||||
const ROOM_TYPE_FOR_TWO = 2;
|
const ROOM_TYPE_FOR_TWO = 2;
|
||||||
const ROOM_TYPE_FOR_THREE = 3;
|
const ROOM_TYPE_FOR_THREE = 3;
|
||||||
const ROOM_TYPE_FOR_FOUR = 4;
|
const ROOM_TYPE_FOR_FOUR = 4;
|
||||||
const ROOM_TYPE_WITH_OTHERS = 5;
|
const ROOM_TYPE_FOR_FIVE = 5;
|
||||||
const ROOM_TYPE_OTHER = 6;
|
const ROOM_TYPE_FOR_SIX = 6;
|
||||||
|
const ROOM_TYPE_FOR_SEVEN = 7;
|
||||||
|
const ROOM_TYPE_FOR_EIGHT = 8;
|
||||||
|
const ROOM_TYPE_FOR_NINE_PLUS = 9;
|
||||||
|
const ROOM_TYPE_WITH_OTHERS = 10;
|
||||||
|
|
||||||
const ROOM_STATUS_AVAILABLE = 1;
|
const ROOM_STATUS_AVAILABLE = 1;
|
||||||
const ROOM_STATUS_ON_REQUEST = 2;
|
const ROOM_STATUS_ON_REQUEST = 2;
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace EP\EpProducts\Domain\Model;
|
|
||||||
|
|
||||||
/***************************************************************
|
|
||||||
*
|
|
||||||
* Copyright notice
|
|
||||||
*
|
|
||||||
* (c) 2016 Björn Fromme <[email protected]>, dreipunktnull
|
|
||||||
*
|
|
||||||
* All rights reserved
|
|
||||||
*
|
|
||||||
* This script is part of the TYPO3 project. The TYPO3 project is
|
|
||||||
* free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The GNU General Public License can be found at
|
|
||||||
* http://www.gnu.org/copyleft/gpl.html.
|
|
||||||
*
|
|
||||||
* This script is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* This copyright notice MUST APPEAR in all copies of the script!
|
|
||||||
***************************************************************/
|
|
||||||
|
|
||||||
class RoomMapping extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $code = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $type = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getCode()
|
|
||||||
{
|
|
||||||
return $this->code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $code
|
|
||||||
*/
|
|
||||||
public function setCode($code)
|
|
||||||
{
|
|
||||||
$this->code = $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getType()
|
|
||||||
{
|
|
||||||
return $this->type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $type
|
|
||||||
*/
|
|
||||||
public function setType($type)
|
|
||||||
{
|
|
||||||
$this->type = $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -76,9 +76,11 @@ class ProductRepository extends AbstractRepository
|
|||||||
'date.services_included as servicesIncluded', 'date.skipass_included as skiPassIncluded',
|
'date.services_included as servicesIncluded', 'date.skipass_included as skiPassIncluded',
|
||||||
'date.bus_available as busAvailable', 'date.new as new',
|
'date.bus_available as busAvailable', 'date.new as new',
|
||||||
'date.min_price as minPrice', 'date.nights as nights', 'date.available as available',
|
'date.min_price as minPrice', 'date.nights as nights', 'date.available as available',
|
||||||
'date.pseudo_price as pseudoPrice', 'date.hide_booking_button as hideBookingButton',
|
'date.pseudo_price as pseudoPrice', 'date.hide_booking_button as hideBookingButton'
|
||||||
'room.type as roomType'
|
|
||||||
)
|
)
|
||||||
|
// The GROUP BY below collapses many room rows per group; an arbitrary
|
||||||
|
// room.type would hide most types from the filter facet.
|
||||||
|
->addSelectLiteral('GROUP_CONCAT(DISTINCT room.type) as roomTypes')
|
||||||
->from('tx_epproducts_domain_model_date', 'date')
|
->from('tx_epproducts_domain_model_date', 'date')
|
||||||
->leftJoin('date', 'tx_epproducts_domain_model_concept', 'con', 'date.concept = con.uid')
|
->leftJoin('date', 'tx_epproducts_domain_model_concept', 'con', 'date.concept = con.uid')
|
||||||
->leftJoin('date', 'tx_epproducts_domain_model_room', 'room', 'room.date = date.uid')
|
->leftJoin('date', 'tx_epproducts_domain_model_room', 'room', 'room.date = date.uid')
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace EP\EpProducts\Domain\Repository;
|
|
||||||
|
|
||||||
/***************************************************************
|
|
||||||
*
|
|
||||||
* Copyright notice
|
|
||||||
*
|
|
||||||
* (c) 2016 Björn Fromme <[email protected]>, dreipunktnull
|
|
||||||
*
|
|
||||||
* All rights reserved
|
|
||||||
*
|
|
||||||
* This script is part of the TYPO3 project. The TYPO3 project is
|
|
||||||
* free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The GNU General Public License can be found at
|
|
||||||
* http://www.gnu.org/copyleft/gpl.html.
|
|
||||||
*
|
|
||||||
* This script is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* This copyright notice MUST APPEAR in all copies of the script!
|
|
||||||
***************************************************************/
|
|
||||||
|
|
||||||
class RoomMappingRepository extends AbstractRepository
|
|
||||||
{}
|
|
||||||
@@ -100,7 +100,23 @@ class ApiClient implements LoggerAwareInterface
|
|||||||
'json' => $addressData,
|
'json' => $addressData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertStatusCode($response, [Response::HTTP_OK, Response::HTTP_CREATED, Response::HTTP_ACCEPTED, Response::HTTP_NO_CONTENT], 'POST', 'contactform');
|
// A conflict means the address is already registered, which is the normal answer to a
|
||||||
|
// resubmitted contact form and not worth reporting.
|
||||||
|
$this->assertStatusCode($response, [Response::HTTP_OK, Response::HTTP_CREATED, Response::HTTP_ACCEPTED, Response::HTTP_NO_CONTENT, Response::HTTP_CONFLICT], 'POST', 'contactform');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function submitApplication(array $applicationData): void
|
||||||
|
{
|
||||||
|
$response = $this->request('POST', 'applications', [
|
||||||
|
'json' => $applicationData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// A conflict means the submission is already known, which is the normal answer to a
|
||||||
|
// double-clicked submit button and not worth reporting.
|
||||||
|
$this->assertStatusCode($response, [Response::HTTP_OK, Response::HTTP_CREATED, Response::HTTP_ACCEPTED, Response::HTTP_NO_CONTENT, Response::HTTP_CONFLICT], 'POST', 'applications');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,11 +60,6 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
*/
|
*/
|
||||||
protected $pid = 0;
|
protected $pid = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $roomMappings = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@@ -90,6 +85,11 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
*/
|
*/
|
||||||
protected $importSourceFactory;
|
protected $importSourceFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var RoomTypeResolver
|
||||||
|
*/
|
||||||
|
protected $roomTypeResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@@ -101,15 +101,18 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
* @param ConfigurationManagerInterface $configurationManager
|
* @param ConfigurationManagerInterface $configurationManager
|
||||||
* @param CacheService $cacheService
|
* @param CacheService $cacheService
|
||||||
* @param ProductImportSourceFactory $importSourceFactory
|
* @param ProductImportSourceFactory $importSourceFactory
|
||||||
|
* @param RoomTypeResolver $roomTypeResolver
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ConfigurationManagerInterface $configurationManager,
|
ConfigurationManagerInterface $configurationManager,
|
||||||
CacheService $cacheService,
|
CacheService $cacheService,
|
||||||
ProductImportSourceFactory $importSourceFactory
|
ProductImportSourceFactory $importSourceFactory,
|
||||||
|
RoomTypeResolver $roomTypeResolver
|
||||||
) {
|
) {
|
||||||
$this->configurationManager = $configurationManager;
|
$this->configurationManager = $configurationManager;
|
||||||
$this->cacheService = $cacheService;
|
$this->cacheService = $cacheService;
|
||||||
$this->importSourceFactory = $importSourceFactory;
|
$this->importSourceFactory = $importSourceFactory;
|
||||||
|
$this->roomTypeResolver = $roomTypeResolver;
|
||||||
|
|
||||||
$settings = GeneralUtility::removeDotsFromTS(
|
$settings = GeneralUtility::removeDotsFromTS(
|
||||||
$this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT)
|
$this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT)
|
||||||
@@ -130,7 +133,6 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
public function import($path): int
|
public function import($path): int
|
||||||
{
|
{
|
||||||
$this->db = $this->getDbConnection();
|
$this->db = $this->getDbConnection();
|
||||||
$this->getRoomMappings();
|
|
||||||
$this->getHotelMappings();
|
$this->getHotelMappings();
|
||||||
$this->importPickups($path);
|
$this->importPickups($path);
|
||||||
$this->createTempTables();
|
$this->createTempTables();
|
||||||
@@ -594,13 +596,9 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$roomTypeKey = mb_strtolower($code);
|
$label = (string) $roomXml->attributes()['zimmertext'];
|
||||||
if (array_key_exists($roomTypeKey, $this->roomMappings)) {
|
$roomMinPax = (int) $roomXml->attributes()['MinPax'];
|
||||||
$roomType = $this->roomMappings[$roomTypeKey];
|
$pax = (int) $roomXml->attributes()['MaxPax'];
|
||||||
} else {
|
|
||||||
$this->logger->warning('room type not found', ['code' => $code]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$available = (int) $roomXml->attributes()['verfuegbar'];
|
$available = (int) $roomXml->attributes()['verfuegbar'];
|
||||||
$price = (int) $roomXml->attributes()['preis'];
|
$price = (int) $roomXml->attributes()['preis'];
|
||||||
@@ -610,12 +608,23 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pax = (int) $roomXml->attributes()['MinPax'];
|
$roomType = $this->roomTypeResolver->resolve($code, $label, $pax, $roomMinPax);
|
||||||
if (0 === $minPax || $pax < $minPax) {
|
|
||||||
$minPax = $pax;
|
if ($pax < 1 && $roomMinPax < 1) {
|
||||||
|
$this->logger->warning('room without occupancy', ['code' => $code, 'zimmertext' => $label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->roomTypeResolver->needsReview($code, $label, $pax)) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'single-occupancy room without single-use marker',
|
||||||
|
['code' => $code, 'zimmertext' => $label]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $minPax || $roomMinPax < $minPax) {
|
||||||
|
$minPax = $roomMinPax;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pax = (int) $roomXml->attributes()['MaxPax'];
|
|
||||||
$availableTotal += $available * $pax;
|
$availableTotal += $available * $pax;
|
||||||
|
|
||||||
// Determine lowest price of available rooms
|
// Determine lowest price of available rooms
|
||||||
@@ -648,7 +657,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
'bus_pro_id' => (int) $roomXml->attributes()['idbuspro_zimmer'],
|
'bus_pro_id' => (int) $roomXml->attributes()['idbuspro_zimmer'],
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
'type' => $roomType,
|
'type' => $roomType,
|
||||||
'name' => (string) $roomXml->attributes()['zimmertext'],
|
'name' => $label,
|
||||||
'pax' => $pax,
|
'pax' => $pax,
|
||||||
'nights' => (int) $roomXml->attributes()['naechte'],
|
'nights' => (int) $roomXml->attributes()['naechte'],
|
||||||
'price' => $price,
|
'price' => $price,
|
||||||
@@ -791,16 +800,6 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRoomMappings(): void
|
|
||||||
{
|
|
||||||
$sql = 'SELECT code, type FROM tx_epproducts_domain_model_roommapping WHERE deleted = 0 AND hidden = 0';
|
|
||||||
$roomMappings = $this->db->fetchAllAssociative($sql);
|
|
||||||
foreach ($roomMappings as $mapping) {
|
|
||||||
$key = trim(mb_strtolower($mapping['code']));
|
|
||||||
$this->roomMappings[$key] = $mapping['type'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getHotelMappings(): void
|
protected function getHotelMappings(): void
|
||||||
{
|
{
|
||||||
$sql = '
|
$sql = '
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace EP\EpProducts\Service;
|
namespace EP\EpProducts\Service;
|
||||||
|
|
||||||
use EP\EpProducts\Domain\Model\Dto\FilterOptions;
|
use EP\EpProducts\Domain\Model\Dto\FilterOptions;
|
||||||
|
use EP\EpProducts\Domain\Model\Room;
|
||||||
use EP\EpProducts\Domain\Model\SearchResult;
|
use EP\EpProducts\Domain\Model\SearchResult;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -212,6 +213,12 @@ class FilterService implements SingletonInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($item['rooms'] as $roomType) {
|
foreach ($item['rooms'] as $roomType) {
|
||||||
|
// Type 0 is not a room (Baby) and must never become a filter option:
|
||||||
|
// it has no label, so it would render as an empty checkbox and leave
|
||||||
|
// a hole in the option list.
|
||||||
|
if ((int) $roomType === Room::ROOM_TYPE_UNCLASSIFIED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!\in_array($roomType, $roomTypes, false)) {
|
if (!\in_array($roomType, $roomTypes, false)) {
|
||||||
$roomTypes[] = $roomType;
|
$roomTypes[] = $roomType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,11 +76,28 @@ class FilterSettingsEncoder
|
|||||||
return implode('|', $encoded);
|
return implode('|', $encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an encoded filter settings string.
|
||||||
|
*
|
||||||
|
* The format is strictly positional, so a string whose field count does not
|
||||||
|
* match the mapping cannot be interpreted: every field after a missing one
|
||||||
|
* would be read into the wrong setting. Such input is rejected outright
|
||||||
|
* rather than silently producing a shifted - and therefore wrong - result.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @return array the decoded settings, or an empty array if $data is malformed
|
||||||
|
*/
|
||||||
public function decode(string $data): array
|
public function decode(string $data): array
|
||||||
{
|
{
|
||||||
$decoded = [];
|
$mapping = $this->getMapping();
|
||||||
$values = explode('|', $data);
|
$values = explode('|', $data);
|
||||||
foreach ($this->getMapping() as $index => $property)
|
|
||||||
|
if (count($values) !== count($mapping)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = [];
|
||||||
|
foreach ($mapping as $index => $property)
|
||||||
{
|
{
|
||||||
[$name, $type] = $property;
|
[$name, $type] = $property;
|
||||||
$value = $values[$index];
|
$value = $values[$index];
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace EP\EpProducts\Service;
|
||||||
|
|
||||||
|
/***************************************************************
|
||||||
|
*
|
||||||
|
* Copyright notice
|
||||||
|
*
|
||||||
|
* (c) 2016 Björn Fromme <[email protected]>, dreipunktnull
|
||||||
|
*
|
||||||
|
* All rights reserved
|
||||||
|
*
|
||||||
|
* This script is part of the TYPO3 project. The TYPO3 project is
|
||||||
|
* free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* The GNU General Public License can be found at
|
||||||
|
* http://www.gnu.org/copyleft/gpl.html.
|
||||||
|
*
|
||||||
|
* This script is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* This copyright notice MUST APPEAR in all copies of the script!
|
||||||
|
***************************************************************/
|
||||||
|
|
||||||
|
use EP\EpProducts\Domain\Model\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the room type from the data each <preis> element carries.
|
||||||
|
*
|
||||||
|
* Replaces the former tx_epproducts_domain_model_roommapping lookup: zimmercode is
|
||||||
|
* neither unique per room nor a function of room type, so a code-keyed table cannot
|
||||||
|
* classify this data. MaxPax is the advertised occupancy and is present on every row.
|
||||||
|
*/
|
||||||
|
class RoomTypeResolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Phrases in zimmertext that mark a bed shared with other customers.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
const LABEL_PATTERNS_WITH_OTHERS = [
|
||||||
|
'bett im',
|
||||||
|
'mit ander',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phrases in zimmertext that mark a deliberate single use of a larger room.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
const LABEL_PATTERNS_SINGLE_USE = [
|
||||||
|
'einzel',
|
||||||
|
'1 person',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $code zimmercode
|
||||||
|
* @param string $label zimmertext
|
||||||
|
* @param int $maxPax MaxPax
|
||||||
|
* @param int $minPax MinPax
|
||||||
|
* @return int one of the Room::ROOM_TYPE_* constants
|
||||||
|
*/
|
||||||
|
public function resolve(string $code, string $label, int $maxPax, int $minPax): int
|
||||||
|
{
|
||||||
|
// Not a room: "Babypreis für 0-2 Jährige". Kept in the date record for its
|
||||||
|
// price and availability, but hidden from the filter.
|
||||||
|
if ($code === DateImportService::CODE_BABYROOM) {
|
||||||
|
return Room::ROOM_TYPE_UNCLASSIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->matchesAny($label, static::LABEL_PATTERNS_WITH_OTHERS)) {
|
||||||
|
return Room::ROOM_TYPE_WITH_OTHERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pax = $maxPax > 0 ? $maxPax : $minPax;
|
||||||
|
|
||||||
|
if ($pax >= 1) {
|
||||||
|
// Type numbering is chosen so that type === pax for pax 1-8.
|
||||||
|
return $pax >= 9 ? Room::ROOM_TYPE_FOR_NINE_PLUS : $pax;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No usable occupancy - should never happen on current data, the caller logs it.
|
||||||
|
return Room::ROOM_TYPE_WITH_OTHERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tripwire for an under-matching label pattern: a row classified as a single room
|
||||||
|
* whose label carries no single-use marker is most likely a shared bed phrased in
|
||||||
|
* a way LABEL_PATTERNS_WITH_OTHERS does not yet cover.
|
||||||
|
*
|
||||||
|
* @param string $code zimmercode
|
||||||
|
* @param string $label zimmertext
|
||||||
|
* @param int $maxPax MaxPax
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function needsReview(string $code, string $label, int $maxPax): bool
|
||||||
|
{
|
||||||
|
if ($this->resolve($code, $label, $maxPax, $maxPax) !== Room::ROOM_TYPE_FOR_ONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($maxPax !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !$this->matchesAny($label, static::LABEL_PATTERNS_SINGLE_USE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $label
|
||||||
|
* @param array $patterns
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function matchesAny(string $label, array $patterns): bool
|
||||||
|
{
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (mb_strpos(mb_strtolower($label), mb_strtolower($pattern)) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ use EP\EpProducts\Domain\Model\SearchResult;
|
|||||||
use EP\EpProducts\Domain\Repository\ProductRepository;
|
use EP\EpProducts\Domain\Repository\ProductRepository;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use TYPO3\CMS\Core\SingletonInterface;
|
use TYPO3\CMS\Core\SingletonInterface;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
class SearchResultService implements SingletonInterface
|
class SearchResultService implements SingletonInterface
|
||||||
{
|
{
|
||||||
@@ -216,8 +217,10 @@ class SearchResultService implements SingletonInterface
|
|||||||
$item['filterMetadata']['boardTypes'][] = $boardType;
|
$item['filterMetadata']['boardTypes'][] = $boardType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($row['roomType']) && !\in_array($row['roomType'], $item['rooms'], false)) {
|
foreach (GeneralUtility::trimExplode(',', (string) ($row['roomTypes'] ?? ''), true) as $roomType) {
|
||||||
$item['rooms'][] = $row['roomType'];
|
if (!\in_array($roomType, $item['rooms'], false)) {
|
||||||
|
$item['rooms'][] = $roomType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!\in_array($row['nights'], $item['nights'], false)) {
|
if (!\in_array($row['nights'], $item['nights'], false)) {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ use EP\EpProducts\Domain\Repository\RegionRepository;
|
|||||||
use EP\EpProducts\Domain\Repository\SnowreportRepository;
|
use EP\EpProducts\Domain\Repository\SnowreportRepository;
|
||||||
use EP\EpProducts\Domain\Repository\WebcamRepository;
|
use EP\EpProducts\Domain\Repository\WebcamRepository;
|
||||||
use EP\EpProducts\Utility\SettingsUtility;
|
use EP\EpProducts\Utility\SettingsUtility;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||||
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
use TYPO3\CMS\Core\SingletonInterface;
|
use TYPO3\CMS\Core\SingletonInterface;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
||||||
@@ -44,14 +47,34 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
const XML_FILE_URL = 'https://www.skiresort-service.com/xml-feed/';
|
const XML_FILE_URL = 'https://www.skiresort-service.com/xml-feed/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* Connection and read timeout for the feed download, in seconds. A healthy response to this
|
||||||
|
* endpoint arrives in well under a second, so the ceiling is kept low enough that an outage on
|
||||||
|
* the vendor side fails visibly instead of stalling the scheduler task or the backend request.
|
||||||
*/
|
*/
|
||||||
protected static $validSnowreportUids = [];
|
const HTTP_CONNECT_TIMEOUT = 5;
|
||||||
|
const HTTP_TIMEOUT = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected static $validWebcamUids = [];
|
protected $validSnowreportUids = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $validWebcamUids = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Existing records keyed by vendor_uid, preloaded once per import to avoid a query per record.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $snowreportIndex = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $webcamIndex = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
|
* @var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
|
||||||
@@ -91,27 +114,62 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
$this->regionRepository = $regionRepository;
|
$this->regionRepository = $regionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TYPE_INT = 'int';
|
||||||
|
const TYPE_STRING = 'string';
|
||||||
|
const TYPE_BOOL = 'bool';
|
||||||
|
const TYPE_DATE = 'date';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Declared schema of the vendor feed: XML element name => target column and its type.
|
||||||
|
*
|
||||||
|
* XML carries no type information - every element is a string - so the type of each field is
|
||||||
|
* declared here rather than guessed from the value. Values are converted strictly according to
|
||||||
|
* this table; anything unexpected is logged and falls back to the type's empty value instead of
|
||||||
|
* silently changing type.
|
||||||
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected static $map = [
|
protected static $map = [
|
||||||
'region_id' => 'vendor_uid',
|
'region_id' => ['column' => 'vendor_uid', 'type' => self::TYPE_INT],
|
||||||
'region_name' => 'region_name',
|
'region_name' => ['column' => 'region_name', 'type' => self::TYPE_STRING],
|
||||||
'region_offen' => 'opened',
|
'region_offen' => ['column' => 'opened', 'type' => self::TYPE_BOOL],
|
||||||
'gletschergebiet' => 'glacier',
|
'gletschergebiet' => ['column' => 'glacier', 'type' => self::TYPE_BOOL],
|
||||||
'schneehoehe_berg' => 'snow_level_mountain',
|
'schneehoehe_berg' => ['column' => 'snow_level_mountain', 'type' => self::TYPE_INT],
|
||||||
'schneehoehe_tal' => 'snow_level_valley',
|
'schneehoehe_tal' => ['column' => 'snow_level_valley', 'type' => self::TYPE_INT],
|
||||||
'schneequalitaet' => 'snow_quality',
|
'schneequalitaet' => ['column' => 'snow_quality', 'type' => self::TYPE_STRING],
|
||||||
'letzter_schneefall' => 'last_snowfall',
|
'letzter_schneefall' => ['column' => 'last_snowfall', 'type' => self::TYPE_DATE],
|
||||||
'lifte_gesamt' => 'lifts_total',
|
'lifte_gesamt' => ['column' => 'lifts_total', 'type' => self::TYPE_INT],
|
||||||
'offene_lifte' => 'lifts_opened',
|
'offene_lifte' => ['column' => 'lifts_opened', 'type' => self::TYPE_INT],
|
||||||
'letzte_aktualisierung' => 'last_update',
|
'letzte_aktualisierung' => ['column' => 'last_update', 'type' => self::TYPE_DATE],
|
||||||
'hoehemax' => 'height_max',
|
'hoehemax' => ['column' => 'height_max', 'type' => self::TYPE_INT],
|
||||||
'hoehemin' => 'height_min',
|
'hoehemin' => ['column' => 'height_min', 'type' => self::TYPE_INT],
|
||||||
'datum_saisonstart' => 'season_start',
|
'datum_saisonstart' => ['column' => 'season_start', 'type' => self::TYPE_DATE],
|
||||||
'datum_saisonende' => 'season_end',
|
'datum_saisonende' => ['column' => 'season_end', 'type' => self::TYPE_DATE],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepted boolean representations, lowercased.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected static $booleanMap = [
|
||||||
|
'true' => true,
|
||||||
|
'1' => true,
|
||||||
|
'ja' => true,
|
||||||
|
'yes' => true,
|
||||||
|
'false' => false,
|
||||||
|
'0' => false,
|
||||||
|
'nein' => false,
|
||||||
|
'no' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distinct conversion problems encountered during the current import.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $conversionWarnings = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
@@ -119,39 +177,41 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
{
|
{
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$file = GeneralUtility::getFileAbsFileName(self::XML_FILE_PATH);
|
$file = GeneralUtility::getFileAbsFileName(self::XML_FILE_PATH);
|
||||||
$xmlContent = file_get_contents($file);
|
|
||||||
$xmlData = simplexml_load_string($xmlContent);
|
if (!is_file($file)) {
|
||||||
|
$this->writelog(sprintf('Snowreport XML import failed: file "%s" does not exist.', $file));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$xmlData = $this->parseReportsXml((string) file_get_contents($file));
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
$this->writelog(sprintf('Snowreport XML import failed: %s', $e->getMessage()));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetImportState();
|
||||||
|
$this->buildIndexes();
|
||||||
|
|
||||||
$snowReportStoragePid = $this->getStoragePid();
|
$snowReportStoragePid = $this->getStoragePid();
|
||||||
|
|
||||||
foreach ($xmlData->schneemeldung as $reportXml)
|
foreach ($xmlData->schneemeldung as $reportXml)
|
||||||
{
|
{
|
||||||
$vendorUid = (int) $reportXml->region_id;
|
$vendorUid = (int) $reportXml->region_id;
|
||||||
$result = $this->snowreportRepository->findByVendorUid($vendorUid);
|
$snowreport = $this->getSnowreport($vendorUid);
|
||||||
if ($result->count() === 0) {
|
|
||||||
$snowreport = new Snowreport();
|
|
||||||
$this->snowreportRepository->add($snowreport);
|
|
||||||
} else {
|
|
||||||
$snowreport = $result->getFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = [];
|
$row = [];
|
||||||
foreach (static::$map as $key => $column)
|
foreach (static::$map as $key => $field)
|
||||||
{
|
{
|
||||||
$valueRaw = trim($reportXml->{$key});
|
if (!isset($reportXml->{$key})) {
|
||||||
|
$this->addConversionWarning(sprintf('Element <%s> missing in feed.', $key));
|
||||||
if ($valueRaw === 'true') {
|
$row[$field['column']] = $this->getEmptyValue($field['type']);
|
||||||
$value = true;
|
continue;
|
||||||
} elseif ($valueRaw === 'false') {
|
|
||||||
$value = false;
|
|
||||||
} elseif (preg_match('/^\d{4}\-\d{2}\-\d{2}$/', $valueRaw)) {
|
|
||||||
$date = new \DateTime($valueRaw);
|
|
||||||
$value = $date->getTimestamp();
|
|
||||||
} else {
|
|
||||||
$value = (string) $valueRaw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$row[$column] = $value;
|
$row[$field['column']] = $this->convertValue((string) $reportXml->{$key}, $field['type'], $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
$snowreport->setPid($snowReportStoragePid);
|
$snowreport->setPid($snowReportStoragePid);
|
||||||
@@ -175,23 +235,24 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
$this->importWebcamData($reportXml->webcam, $snowreport);
|
$this->importWebcamData($reportXml->webcam, $snowreport);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($snowreport->_isNew()) {
|
// New objects are already registered via add(); only managed ones need update().
|
||||||
$this->persistenceManager->persistAll();
|
if (!$snowreport->_isNew()) {
|
||||||
} else {
|
|
||||||
$this->snowreportRepository->update($snowreport);
|
$this->snowreportRepository->update($snowreport);
|
||||||
}
|
}
|
||||||
|
|
||||||
static::$validSnowreportUids[] = $vendorUid;
|
$this->validSnowreportUids[] = $vendorUid;
|
||||||
|
|
||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->persistenceManager->persistAll();
|
$this->persistenceManager->persistAll();
|
||||||
|
|
||||||
$this->cleanupData('tx_epproducts_domain_model_webcam', static::$validWebcamUids);
|
$this->cleanupData('tx_epproducts_domain_model_webcam', $this->validWebcamUids);
|
||||||
$this->cleanupData('tx_epproducts_domain_model_snowreport', static::$validSnowreportUids);
|
$this->cleanupData('tx_epproducts_domain_model_snowreport', $this->validSnowreportUids);
|
||||||
|
$this->updateWebcamCounters();
|
||||||
|
|
||||||
$GLOBALS['BE_USER']->writelog(4, 0, 0, 0, sprintf('Snowreport XML import successful (%d records).', $count), []);
|
$this->logConversionWarnings();
|
||||||
|
$this->writelog(sprintf('Snowreport XML import successful (%d records).', $count));
|
||||||
|
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
@@ -205,40 +266,52 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
$snowReportStoragePid = $this->getStoragePid();
|
$snowReportStoragePid = $this->getStoragePid();
|
||||||
foreach ($xml->children() as $item)
|
foreach ($xml->children() as $item)
|
||||||
{
|
{
|
||||||
$include = (string) $item->aktuell === 'true';
|
$include = $this->convertValue((string) $item->aktuell, self::TYPE_BOOL, 'aktuell');
|
||||||
$available = (string) $item->verf === 'true';
|
$available = $this->convertValue((string) $item->verf, self::TYPE_BOOL, 'verf');
|
||||||
$vendorUid = (int) $item->uid;
|
$vendorUid = (int) $item->uid;
|
||||||
$result = $this->webcamRepository->findByVendorUid($vendorUid);
|
|
||||||
// Skip invalid cams
|
$webcam = isset($this->webcamIndex[$vendorUid]) ? $this->webcamIndex[$vendorUid] : null;
|
||||||
if ($include === false && $result->count() === 0) {
|
|
||||||
|
// Skip cams that are neither current nor already known
|
||||||
|
if (null === $webcam && false === $include) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new cam if not in db yet
|
// Add new cam if not in db yet
|
||||||
if ($result->count() === 0) {
|
if (null === $webcam) {
|
||||||
$webcam = new Webcam();
|
$webcam = new Webcam();
|
||||||
$webcam->setVendorUid($vendorUid);
|
$webcam->setVendorUid($vendorUid);
|
||||||
$webcam->setPid($snowReportStoragePid);
|
$webcam->setPid($snowReportStoragePid);
|
||||||
$webcam->setUrl((string) $item->url);
|
|
||||||
$webcam->setName((string) $item->dt);
|
|
||||||
$webcam->setAvailable($available);
|
|
||||||
$this->webcamRepository->add($webcam);
|
$this->webcamRepository->add($webcam);
|
||||||
|
$this->webcamIndex[$vendorUid] = $webcam;
|
||||||
$snowreport->addWebcam($webcam);
|
$snowreport->addWebcam($webcam);
|
||||||
} else {
|
|
||||||
// Update cam in db and set availability
|
|
||||||
$webcam = $result->getFirst();
|
|
||||||
$webcam->setUrl((string) $item->url);
|
|
||||||
$webcam->setName((string) $item->dt);
|
|
||||||
$webcam->setAvailable($available && $include);
|
|
||||||
}
|
}
|
||||||
static::$validWebcamUids[] = $vendorUid;
|
|
||||||
|
$webcam->setUrl((string) $item->url);
|
||||||
|
$webcam->setName((string) $item->dt);
|
||||||
|
$webcam->setAvailable($available && $include);
|
||||||
|
|
||||||
|
$this->validWebcamUids[] = $vendorUid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags records that are no longer contained in the feed as deleted.
|
||||||
|
*
|
||||||
|
* @param string $table
|
||||||
|
* @param array $validUids
|
||||||
|
* @return int Number of records flagged
|
||||||
|
*/
|
||||||
public function cleanupData($table, array $validUids)
|
public function cleanupData($table, array $validUids)
|
||||||
{
|
{
|
||||||
/** @var ConnectionPool $connectionPool */
|
/** @var ConnectionPool $connectionPool */
|
||||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||||
$qb = $connectionPool->getQueryBuilderForTable($table);
|
$qb = $connectionPool->getQueryBuilderForTable($table);
|
||||||
|
// Hidden records are still managed by the import, only deleted ones are out of scope.
|
||||||
|
$qb->getRestrictions()
|
||||||
|
->removeAll()
|
||||||
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||||
|
;
|
||||||
$result = $qb
|
$result = $qb
|
||||||
->select('vendor_uid')
|
->select('vendor_uid')
|
||||||
->from($table)
|
->from($table)
|
||||||
@@ -250,36 +323,293 @@ class SnowreportImportService implements SingletonInterface
|
|||||||
return (int)$row['vendor_uid'];
|
return (int)$row['vendor_uid'];
|
||||||
}, $result);
|
}, $result);
|
||||||
|
|
||||||
$obsoleteUids = array_diff($currentUids, $validUids);
|
$obsoleteUids = array_values(array_diff($currentUids, $validUids));
|
||||||
if (0 === count($obsoleteUids)) {
|
if (0 === count($obsoleteUids)) {
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb = $connectionPool->getQueryBuilderForTable($table);
|
$qb = $connectionPool->getQueryBuilderForTable($table);
|
||||||
$qb
|
|
||||||
->delete($table)
|
return (int) $qb
|
||||||
->where($qb->expr()->in('vendor_uid', $obsoleteUids))
|
->update($table)
|
||||||
|
->set('deleted', 1)
|
||||||
|
->set('tstamp', isset($GLOBALS['EXEC_TIME']) ? $GLOBALS['EXEC_TIME'] : time())
|
||||||
|
->where(
|
||||||
|
$qb->expr()->in(
|
||||||
|
'vendor_uid',
|
||||||
|
$qb->createNamedParameter($obsoleteUids, Connection::PARAM_INT_ARRAY)
|
||||||
|
)
|
||||||
|
)
|
||||||
->execute()
|
->execute()
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates the inline child counter on the snowreport records. Webcams are flagged as deleted
|
||||||
|
* with plain SQL, which bypasses the counter Extbase maintains for the relation.
|
||||||
|
*/
|
||||||
|
protected function updateWebcamCounters()
|
||||||
|
{
|
||||||
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getConnectionForTable('tx_epproducts_domain_model_snowreport');
|
||||||
|
|
||||||
|
$connection->executeStatement(
|
||||||
|
'UPDATE tx_epproducts_domain_model_snowreport s SET s.webcams = ('
|
||||||
|
. ' SELECT COUNT(*) FROM tx_epproducts_domain_model_webcam w'
|
||||||
|
. ' WHERE w.snowreport = s.uid AND w.deleted = 0'
|
||||||
|
. ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the feed and replaces the local file only if the response is a usable XML document.
|
||||||
|
* The existing file is left untouched on any failure.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
public function downloadReportsXml()
|
public function downloadReportsXml()
|
||||||
{
|
{
|
||||||
$path = GeneralUtility::getFileAbsFileName(self::XML_FILE_PATH);
|
$path = GeneralUtility::getFileAbsFileName(self::XML_FILE_PATH);
|
||||||
$backupPath = $path . '.bak';
|
|
||||||
|
|
||||||
@copy($path, $backupPath);
|
try {
|
||||||
@unlink($path);
|
$xmlContent = $this->fetchReportsXml();
|
||||||
|
$this->parseReportsXml($xmlContent);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->writelog(sprintf(
|
||||||
|
'Snowreport XML download failed (%s). Keeping existing file.',
|
||||||
|
$e->getMessage()
|
||||||
|
));
|
||||||
|
|
||||||
$url = self::XML_FILE_URL;
|
return false;
|
||||||
exec("wget -O {$path} {$url}", $output, $return);
|
}
|
||||||
if (!$return) {
|
|
||||||
@unlink($backupPath);
|
// Write to a temporary file first and move it into place, so the feed is either the old or the
|
||||||
|
// new document, never a partially written one.
|
||||||
|
$temporaryPath = $path . '.tmp';
|
||||||
|
if (false === file_put_contents($temporaryPath, $xmlContent) || !rename($temporaryPath, $path)) {
|
||||||
|
@unlink($temporaryPath);
|
||||||
|
$this->writelog(sprintf('Snowreport XML could not be written to "%s".', $path));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
protected function fetchReportsXml()
|
||||||
|
{
|
||||||
|
/** @var RequestFactory $requestFactory */
|
||||||
|
$requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
|
||||||
|
|
||||||
|
$response = $requestFactory->request(self::XML_FILE_URL, 'GET', [
|
||||||
|
'connect_timeout' => self::HTTP_CONNECT_TIMEOUT,
|
||||||
|
'timeout' => self::HTTP_TIMEOUT,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/xml, text/xml',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
sprintf('unexpected status code %d', $response->getStatusCode()),
|
||||||
|
1758000001
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $response->getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the feed and rejects documents that are unusable. Without this an empty or truncated
|
||||||
|
* response would be imported as an empty feed, and the cleanup would flag every record as deleted.
|
||||||
|
*
|
||||||
|
* @param string $xmlContent
|
||||||
|
* @return \SimpleXMLElement
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
protected function parseReportsXml($xmlContent)
|
||||||
|
{
|
||||||
|
if ('' === trim($xmlContent)) {
|
||||||
|
throw new \RuntimeException('response is empty', 1758000002);
|
||||||
|
}
|
||||||
|
|
||||||
|
$useInternalErrors = libxml_use_internal_errors(true);
|
||||||
|
libxml_clear_errors();
|
||||||
|
$xmlData = simplexml_load_string($xmlContent);
|
||||||
|
$errors = libxml_get_errors();
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($useInternalErrors);
|
||||||
|
|
||||||
|
if (false === $xmlData) {
|
||||||
|
$message = count($errors) > 0 ? trim($errors[0]->message) : 'unknown error';
|
||||||
|
throw new \RuntimeException(sprintf('response is not valid XML (%s)', $message), 1758000003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $xmlData->schneemeldung->count()) {
|
||||||
|
throw new \RuntimeException('response contains no <schneemeldung> elements', 1758000004);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xmlData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads existing records once, keyed by vendor_uid. Replaces a repository lookup per feed record.
|
||||||
|
*/
|
||||||
|
protected function buildIndexes()
|
||||||
|
{
|
||||||
|
$this->snowreportIndex = [];
|
||||||
|
foreach ($this->snowreportRepository->findAll() as $snowreport) {
|
||||||
|
$this->snowreportIndex[(int) $snowreport->getVendorUid()] = $snowreport;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webcamIndex = [];
|
||||||
|
foreach ($this->webcamRepository->findAll() as $webcam) {
|
||||||
|
$this->webcamIndex[(int) $webcam->getVendorUid()] = $webcam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $vendorUid
|
||||||
|
* @return Snowreport
|
||||||
|
*/
|
||||||
|
protected function getSnowreport($vendorUid)
|
||||||
|
{
|
||||||
|
if (isset($this->snowreportIndex[$vendorUid])) {
|
||||||
|
return $this->snowreportIndex[$vendorUid];
|
||||||
|
}
|
||||||
|
|
||||||
|
$snowreport = new Snowreport();
|
||||||
|
$snowreport->setVendorUid($vendorUid);
|
||||||
|
$this->snowreportRepository->add($snowreport);
|
||||||
|
$this->snowreportIndex[$vendorUid] = $snowreport;
|
||||||
|
|
||||||
|
return $snowreport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is a singleton, so per-import state must not survive into the next run.
|
||||||
|
*/
|
||||||
|
protected function resetImportState()
|
||||||
|
{
|
||||||
|
$this->validSnowreportUids = [];
|
||||||
|
$this->validWebcamUids = [];
|
||||||
|
$this->conversionWarnings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a raw XML string to the declared type of the field.
|
||||||
|
*
|
||||||
|
* @param string $raw
|
||||||
|
* @param string $type
|
||||||
|
* @param string $key XML element name, for logging
|
||||||
|
* @return int|string|bool
|
||||||
|
*/
|
||||||
|
protected function convertValue($raw, $type, $key)
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// The feed delivers empty elements for data that is not available yet (snow levels out of
|
||||||
|
// season, for example). That is expected, not an error.
|
||||||
|
if ($raw === '') {
|
||||||
|
return $this->getEmptyValue($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case self::TYPE_STRING:
|
||||||
|
return $raw;
|
||||||
|
|
||||||
|
case self::TYPE_INT:
|
||||||
|
if (!preg_match('/^-?\d+$/', $raw)) {
|
||||||
|
$this->addConversionWarning(sprintf('<%s> expected an integer, got "%s".', $key, $raw));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $raw;
|
||||||
|
|
||||||
|
case self::TYPE_BOOL:
|
||||||
|
$normalized = strtolower($raw);
|
||||||
|
if (!array_key_exists($normalized, static::$booleanMap)) {
|
||||||
|
$this->addConversionWarning(sprintf('<%s> expected a boolean, got "%s".', $key, $raw));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::$booleanMap[$normalized];
|
||||||
|
|
||||||
|
case self::TYPE_DATE:
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?$/', $raw)) {
|
||||||
|
$this->addConversionWarning(sprintf('<%s> expected a date, got "%s".', $key, $raw));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = new \DateTime($raw);
|
||||||
|
|
||||||
|
return $date->getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException(sprintf('Unknown field type "%s" declared for <%s>.', $type, $key), 1758000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $type
|
||||||
|
* @return int|string|bool
|
||||||
|
*/
|
||||||
|
protected function getEmptyValue($type)
|
||||||
|
{
|
||||||
|
if ($type === self::TYPE_STRING) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if ($type === self::TYPE_BOOL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
protected function addConversionWarning($message)
|
||||||
|
{
|
||||||
|
// Keyed to keep one entry per distinct problem instead of one per record.
|
||||||
|
$this->conversionWarnings[$message] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a single summary entry to the log if the feed contained unexpected values.
|
||||||
|
*/
|
||||||
|
protected function logConversionWarnings()
|
||||||
|
{
|
||||||
|
if (0 === count($this->conversionWarnings)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$GLOBALS['BE_USER']->writelog(4, 0, 0, 0, 'Snowreport XML download failed. Keeping existing file.', []);
|
$messages = array_keys($this->conversionWarnings);
|
||||||
@rename($backupPath, $path);
|
$this->writelog(sprintf(
|
||||||
|
'Snowreport XML import: %d unexpected value(s) in feed: %s',
|
||||||
|
count($messages),
|
||||||
|
implode(' | ', array_slice($messages, 0, 10))
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->conversionWarnings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scheduler task and the backend module provide a backend user, the CLI command does not.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
protected function writelog($message)
|
||||||
|
{
|
||||||
|
if (isset($GLOBALS['BE_USER']) && is_object($GLOBALS['BE_USER'])) {
|
||||||
|
$GLOBALS['BE_USER']->writelog(4, 0, 0, 0, $message, []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStoragePid()
|
public function getStoragePid()
|
||||||
|
|||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
<?php
|
|
||||||
return [
|
|
||||||
'ctrl' => [
|
|
||||||
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_roommapping',
|
|
||||||
'label' => 'code',
|
|
||||||
'prependAtCopy' => '[COPY %s]',
|
|
||||||
'default_sortby' => 'ORDER BY code',
|
|
||||||
'tstamp' => 'tstamp',
|
|
||||||
'crdate' => 'crdate',
|
|
||||||
'cruser_id' => 'cruser_id',
|
|
||||||
'dividers2tabs' => true,
|
|
||||||
'versioningWS' => false,
|
|
||||||
'languageField' => 'sys_language_uid',
|
|
||||||
'transOrigPointerField' => 'l10n_parent',
|
|
||||||
'transOrigDiffSourceField' => 'l10n_diffsource',
|
|
||||||
'delete' => 'deleted',
|
|
||||||
'enablecolumns' => [
|
|
||||||
'disabled' => 'hidden',
|
|
||||||
'starttime' => 'starttime',
|
|
||||||
'endtime' => 'endtime',
|
|
||||||
],
|
|
||||||
'searchFields' => 'code',
|
|
||||||
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
|
|
||||||
],
|
|
||||||
'types' => [
|
|
||||||
'1' => ['showitem' => 'code, type'],
|
|
||||||
],
|
|
||||||
'palettes' => [
|
|
||||||
'1' => ['showitem' => ''],
|
|
||||||
],
|
|
||||||
'columns' => [
|
|
||||||
|
|
||||||
'sys_language_uid' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'select',
|
|
||||||
'default' => 0,
|
|
||||||
'renderType' => 'selectSingle',
|
|
||||||
'foreign_table' => 'sys_language',
|
|
||||||
'foreign_table_where' => 'ORDER BY sys_language.title',
|
|
||||||
'items' => [
|
|
||||||
['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages', -1],
|
|
||||||
['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.default_value', 0]
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'l10n_parent' => [
|
|
||||||
'displayCond' => 'FIELD:sys_language_uid:>:0',
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'select',
|
|
||||||
'default' => 0,
|
|
||||||
'renderType' => 'selectSingle',
|
|
||||||
'items' => [
|
|
||||||
['', 0],
|
|
||||||
],
|
|
||||||
'foreign_table' => 'tx_epproducts_domain_model_roommapping',
|
|
||||||
'foreign_table_where' => 'AND tx_epproducts_domain_model_roommapping.pid=###CURRENT_PID### AND tx_epproducts_domain_model_roommapping.sys_language_uid IN (-1,0)',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'l10n_diffsource' => [
|
|
||||||
'config' => [
|
|
||||||
'type' => 'passthrough',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
't3ver_label' => [
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.versionLabel',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'input',
|
|
||||||
'size' => 30,
|
|
||||||
'max' => 255,
|
|
||||||
]
|
|
||||||
],
|
|
||||||
|
|
||||||
'hidden' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hidden',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'check',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'starttime' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'input',
|
|
||||||
'size' => 13,
|
|
||||||
'eval' => 'datetime',
|
|
||||||
'checkbox' => 0,
|
|
||||||
'default' => 0,
|
|
||||||
'behaviour' => [
|
|
||||||
'allowLanguageSynchronization' => true,
|
|
||||||
],
|
|
||||||
'renderType' => 'inputDateTime',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'endtime' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'input',
|
|
||||||
'size' => 13,
|
|
||||||
'eval' => 'datetime',
|
|
||||||
'checkbox' => 0,
|
|
||||||
'default' => 0,
|
|
||||||
'behaviour' => [
|
|
||||||
'allowLanguageSynchronization' => true,
|
|
||||||
],
|
|
||||||
'renderType' => 'inputDateTime',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'code' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'Code',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'input',
|
|
||||||
'size' => 30,
|
|
||||||
'eval' => 'trim'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'type' => [
|
|
||||||
'exclude' => 0,
|
|
||||||
'label' => 'Typ',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'select',
|
|
||||||
'renderType' => 'selectSingle',
|
|
||||||
'items' => [
|
|
||||||
['Einzelzimmer', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_FOR_ONE],
|
|
||||||
['Doppelzimmer und Apartm. f. 2 Pers.', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_FOR_TWO],
|
|
||||||
['Zimmer und Apartm. f. 3 Pers.', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_FOR_THREE],
|
|
||||||
['Zimmer und Apartm. f. 4 Pers.', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_FOR_FOUR],
|
|
||||||
['Zimmer und Apartm. mit Anderen', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_WITH_OTHERS],
|
|
||||||
['weitere Zimmer und Apartments', \EP\EpProducts\Domain\Model\Room::ROOM_TYPE_OTHER],
|
|
||||||
],
|
|
||||||
'size' => 1,
|
|
||||||
'minItems' => 1,
|
|
||||||
'maxItems' => 1
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
@@ -39,9 +39,6 @@
|
|||||||
<trans-unit id="tx_epproducts_domain_model_teammember">
|
<trans-unit id="tx_epproducts_domain_model_teammember">
|
||||||
<source>Mitarbeiter/Teamer</source>
|
<source>Mitarbeiter/Teamer</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_epproducts_domain_model_roommapping">
|
|
||||||
<source>Zimmermapping</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="tx_epproducts_domain_model_journey">
|
<trans-unit id="tx_epproducts_domain_model_journey">
|
||||||
<source>Anreise</source>
|
<source>Anreise</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|||||||
@@ -494,47 +494,6 @@ CREATE TABLE tx_epproducts_domain_model_room
|
|||||||
KEY constraints (date, type)
|
KEY constraints (date, type)
|
||||||
);
|
);
|
||||||
|
|
||||||
#
|
|
||||||
# Table structure for table 'tx_epproducts_domain_model_roommapping'
|
|
||||||
#
|
|
||||||
CREATE TABLE tx_epproducts_domain_model_roommapping
|
|
||||||
(
|
|
||||||
|
|
||||||
uid int(11) NOT NULL auto_increment,
|
|
||||||
pid int(11) DEFAULT '0' NOT NULL,
|
|
||||||
|
|
||||||
code varchar(255) DEFAULT '' NOT NULL,
|
|
||||||
type int(11) DEFAULT '0' NOT NULL,
|
|
||||||
|
|
||||||
tstamp int(11) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
crdate int(11) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
cruser_id int(11) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
deleted tinyint(4) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
hidden tinyint(4) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
starttime int(11) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
endtime int(11) unsigned DEFAULT '0' NOT NULL,
|
|
||||||
|
|
||||||
t3ver_oid int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_id int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_wsid int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_label varchar(255) DEFAULT '' NOT NULL,
|
|
||||||
t3ver_state tinyint(4) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_stage int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_count int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_tstamp int(11) DEFAULT '0' NOT NULL,
|
|
||||||
t3ver_move_id int(11) DEFAULT '0' NOT NULL,
|
|
||||||
|
|
||||||
sys_language_uid int(11) DEFAULT '0' NOT NULL,
|
|
||||||
l10n_parent int(11) DEFAULT '0' NOT NULL,
|
|
||||||
l10n_diffsource mediumblob,
|
|
||||||
|
|
||||||
PRIMARY KEY (uid),
|
|
||||||
KEY parent (pid),
|
|
||||||
KEY t3ver_oid (t3ver_oid, t3ver_wsid),
|
|
||||||
KEY language (l10n_parent, sys_language_uid)
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Table structure for table 'tx_epproducts_domain_model_concept'
|
# Table structure for table 'tx_epproducts_domain_model_concept'
|
||||||
#
|
#
|
||||||
@@ -1098,6 +1057,7 @@ CREATE TABLE tx_epproducts_domain_model_snowreport
|
|||||||
|
|
||||||
PRIMARY KEY (uid),
|
PRIMARY KEY (uid),
|
||||||
KEY parent (pid),
|
KEY parent (pid),
|
||||||
|
KEY vendor_uid (vendor_uid),
|
||||||
KEY t3ver_oid (t3ver_oid, t3ver_wsid),
|
KEY t3ver_oid (t3ver_oid, t3ver_wsid),
|
||||||
KEY language (l10n_parent, sys_language_uid)
|
KEY language (l10n_parent, sys_language_uid)
|
||||||
|
|
||||||
@@ -1142,6 +1102,7 @@ CREATE TABLE tx_epproducts_domain_model_webcam
|
|||||||
|
|
||||||
PRIMARY KEY (uid),
|
PRIMARY KEY (uid),
|
||||||
KEY parent (pid),
|
KEY parent (pid),
|
||||||
|
KEY vendor_uid (vendor_uid),
|
||||||
KEY t3ver_oid (t3ver_oid, t3ver_wsid),
|
KEY t3ver_oid (t3ver_oid, t3ver_wsid),
|
||||||
KEY language (l10n_parent, sys_language_uid)
|
KEY language (l10n_parent, sys_language_uid)
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,43 @@ namespace EP\EpTheme\Form;
|
|||||||
|
|
||||||
use TYPO3\CMS\Core\Log\LogManager;
|
use TYPO3\CMS\Core\Log\LogManager;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||||
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
|
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
|
||||||
|
|
||||||
abstract class AbstractMappedFinisher extends AbstractFinisher
|
abstract class AbstractMappedFinisher extends AbstractFinisher
|
||||||
{
|
{
|
||||||
protected function buildPayload(): array
|
/**
|
||||||
|
* Resolves the 'mapping' option into remote name => local form element identifier.
|
||||||
|
*/
|
||||||
|
protected function parseMapping(): array
|
||||||
{
|
{
|
||||||
$values = [];
|
$mappings = [];
|
||||||
$formValues = $this->finisherContext->getFormValues();
|
|
||||||
$mappingOption = (string)$this->parseOption('mapping');
|
$mappingOption = (string)$this->parseOption('mapping');
|
||||||
$mappings = GeneralUtility::trimExplode(',', $mappingOption, true);
|
|
||||||
|
|
||||||
foreach ($mappings as $mapping) {
|
foreach (GeneralUtility::trimExplode(',', $mappingOption, true) as $mapping) {
|
||||||
$parts = GeneralUtility::trimExplode(':', $mapping, true, 2);
|
$parts = GeneralUtility::trimExplode(':', $mapping, true, 2);
|
||||||
if (2 !== count($parts)) {
|
if (2 !== count($parts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[$remote, $local] = $parts;
|
[$remote, $local] = $parts;
|
||||||
if ('' === $remote || '' === $local || false === array_key_exists($local, $formValues)) {
|
if ('' === $remote || '' === $local) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings[$remote] = $local;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildPayload(): array
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
$formValues = $this->finisherContext->getFormValues();
|
||||||
|
|
||||||
|
foreach ($this->parseMapping() as $remote => $local) {
|
||||||
|
if (false === array_key_exists($local, $formValues)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +50,95 @@ abstract class AbstractMappedFinisher extends AbstractFinisher
|
|||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every submitted value that the mapping does not already cover, keyed by the element label
|
||||||
|
* as the editor sees it in the form editor. Forms built in the backend use generated
|
||||||
|
* identifiers (text-1, singleselect-2, ...) that carry no meaning and cannot be renamed
|
||||||
|
* without breaking existing finisher options, so the label is the only stable description
|
||||||
|
* of what was asked.
|
||||||
|
*
|
||||||
|
* The label is taken from the form definition, not through TranslationService: a translated
|
||||||
|
* label would change the payload keys with the site language, and these keys are stored.
|
||||||
|
*/
|
||||||
|
protected function buildLabeledValues(): array
|
||||||
|
{
|
||||||
|
$formValues = $this->finisherContext->getFormValues();
|
||||||
|
$skip = array_merge(
|
||||||
|
array_values($this->parseMapping()),
|
||||||
|
GeneralUtility::trimExplode(',', (string)$this->parseOption('exclude'), true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keyed by identifier in form order, and contains only real form elements - iterating
|
||||||
|
// this instead of the form values keeps the collection in the order of the form and
|
||||||
|
// leaves out anything that is not an element the visitor filled in.
|
||||||
|
$elements = $this->finisherContext->getFormRuntime()->getFormDefinition()->getElements();
|
||||||
|
|
||||||
|
$collected = [];
|
||||||
|
foreach ($elements as $identifier => $element) {
|
||||||
|
if (true === in_array($identifier, $skip, true) || false === array_key_exists($identifier, $formValues)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->normalizeValue($formValues[$identifier], $identifier);
|
||||||
|
if (null === $value || '' === $value || [] === $value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = trim($element->getLabel());
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $identifier;
|
||||||
|
}
|
||||||
|
if (true === array_key_exists($label, $collected)) {
|
||||||
|
$label .= ' (' . $identifier . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$collected[$label] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $collected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @return mixed null when the value has no JSON representation
|
||||||
|
*/
|
||||||
|
protected function normalizeValue($value, string $identifier)
|
||||||
|
{
|
||||||
|
if (true === is_array($value)) {
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($value as $item) {
|
||||||
|
$item = $this->normalizeValue($item, $identifier);
|
||||||
|
if (null !== $item && '' !== $item) {
|
||||||
|
$normalized[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof \DateTimeInterface) {
|
||||||
|
return $value->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof FileReference) {
|
||||||
|
$resource = $value->getOriginalResource();
|
||||||
|
|
||||||
|
return null !== $resource ? $resource->getName() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === is_object($value)) {
|
||||||
|
$this->logWarning('Form value skipped while collecting labeled values: unsupported type.', [
|
||||||
|
'element' => $identifier,
|
||||||
|
'class' => get_class($value),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
protected function logWarning(string $message, array $context = []): void
|
protected function logWarning(string $message, array $context = []): void
|
||||||
{
|
{
|
||||||
GeneralUtility::makeInstance(LogManager::class)
|
GeneralUtility::makeInstance(LogManager::class)
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace EP\EpTheme\Form;
|
||||||
|
|
||||||
|
use EP\EpProducts\MyEP\ApiClient;
|
||||||
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
class ApplicationApiFinisher extends AbstractMappedFinisher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ApiClient
|
||||||
|
*/
|
||||||
|
protected $apiClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiClient $apiClient
|
||||||
|
*/
|
||||||
|
public function injectApiClient(ApiClient $apiClient)
|
||||||
|
{
|
||||||
|
$this->apiClient = $apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function executeInternal()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$payload = $this->buildPayload();
|
||||||
|
if ([] === $payload) {
|
||||||
|
$this->logWarning('Application API finisher skipped: the mapping produced no values.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formDefinition = $this->finisherContext->getFormRuntime()->getFormDefinition();
|
||||||
|
$formLabel = trim($formDefinition->getLabel());
|
||||||
|
|
||||||
|
$payload['form'] = $formDefinition->getIdentifier();
|
||||||
|
// The name the editor gave the form, so the receiving side can label a submission
|
||||||
|
// without keeping a list of identifiers. Taken from the definition rather than
|
||||||
|
// through TranslationService: a translated label would differ per site language.
|
||||||
|
$payload['formLabel'] = '' !== $formLabel ? $formLabel : $formDefinition->getIdentifier();
|
||||||
|
$payload['submittedAt'] = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM);
|
||||||
|
$payload = array_merge($payload, $this->resolvePageContext());
|
||||||
|
$payload['details'] = $this->buildLabeledValues();
|
||||||
|
|
||||||
|
$this->apiClient->submitApplication($payload);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logWarning('Application API finisher request failed.', [
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The page the form was submitted from. The URL is generated through the site router in the
|
||||||
|
* visitor's language, the same way AddReferrerFinisher does it, so it is the page's canonical
|
||||||
|
* URL rather than the submit request with its form arguments attached.
|
||||||
|
*/
|
||||||
|
private function resolvePageContext(): array
|
||||||
|
{
|
||||||
|
if (false === isset($GLOBALS['TSFE']) || 0 === (int)$GLOBALS['TSFE']->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageUid = (int)$GLOBALS['TSFE']->id;
|
||||||
|
$context = ['pageUid' => $pageUid];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageUid);
|
||||||
|
$context['pageUrl'] = (string)$site->getRouter()->generateUri($pageUid, [
|
||||||
|
'_language' => $GLOBALS['TSFE']->getLanguage(),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// An unroutable page must not cost the submission, so the uid is sent without a URL.
|
||||||
|
$this->logWarning('Application API finisher could not resolve the page URL.', [
|
||||||
|
'pageUid' => $pageUid,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace EP\EpTheme\ViewHelpers;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Fluid\ViewHelpers\ImageViewHelper;
|
|
||||||
|
|
||||||
class LazyLoadImageViewHelper extends ImageViewHelper
|
|
||||||
{
|
|
||||||
public function initializeArguments()
|
|
||||||
{
|
|
||||||
parent::initializeArguments();
|
|
||||||
$this->registerArgument('loaderimg', 'string', '', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): string
|
|
||||||
{
|
|
||||||
parent::render();
|
|
||||||
|
|
||||||
$src = $this->tag->getAttribute('src');
|
|
||||||
$this->tag->removeAttribute('src');
|
|
||||||
$this->tag->addAttribute('data', ['src' => $src], false);
|
|
||||||
|
|
||||||
if (isset($this->arguments['loaderimg'])) {
|
|
||||||
$loaderImage = $this->imageService->getImage($this->arguments['loaderimg'], null, false);
|
|
||||||
$processingInstructions = [
|
|
||||||
'width' => $this->arguments['width'],
|
|
||||||
'height' => $this->arguments['height'],
|
|
||||||
'minWidth' => $this->arguments['minWidth'],
|
|
||||||
'minHeight' => $this->arguments['minHeight'],
|
|
||||||
'maxWidth' => $this->arguments['maxWidth'],
|
|
||||||
'maxHeight' => $this->arguments['maxHeight'],
|
|
||||||
];
|
|
||||||
$processedLoaderImage = $this->imageService->applyProcessingInstructions($loaderImage, $processingInstructions);
|
|
||||||
$loaderImageUri = $this->imageService->getImageUri($processedLoaderImage, $this->arguments['absolute']);
|
|
||||||
$this->tag->addAttribute('src', $loaderImageUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
$classes = $this->tag->getAttribute('class');
|
|
||||||
$classItems = GeneralUtility::trimExplode(' ', $classes, true);
|
|
||||||
$classItems[] = 'lazyload';
|
|
||||||
$classes = implode(' ', array_unique($classItems));
|
|
||||||
$this->tag->addAttribute('class', $classes);
|
|
||||||
// ensure a11y compliance
|
|
||||||
$this->tag->removeAttribute('title');
|
|
||||||
|
|
||||||
return $this->tag->render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
@@ -17,5 +17,8 @@ plugin.tx_epproducts {
|
|||||||
ratingWidgetLabel = {$plugin.tx_eptheme.settings.ratingWidgetLabel}
|
ratingWidgetLabel = {$plugin.tx_eptheme.settings.ratingWidgetLabel}
|
||||||
skipassPageUid = {$plugin.tx_eptheme.settings.skipassPageUid}
|
skipassPageUid = {$plugin.tx_eptheme.settings.skipassPageUid}
|
||||||
conceptsPageUid = {$plugin.tx_eptheme.settings.conceptsPageUid}
|
conceptsPageUid = {$plugin.tx_eptheme.settings.conceptsPageUid}
|
||||||
|
mapTileUrl = {$plugin.tx_eptheme.settings.mapTileUrl}
|
||||||
|
mapApiKey = {$plugin.tx_eptheme.settings.mapApiKey}
|
||||||
|
mapAttribution = {$plugin.tx_eptheme.settings.mapAttribution}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -7,5 +7,8 @@ lib.contentElement {
|
|||||||
settings < plugin.tx_eptheme.settings
|
settings < plugin.tx_eptheme.settings
|
||||||
settings.themekey = {$plugin.tx_eptheme.settings.themekey}
|
settings.themekey = {$plugin.tx_eptheme.settings.themekey}
|
||||||
settings.defaultAjaxUid = {$plugin.tx_eptheme.settings.defaultAjaxUid}
|
settings.defaultAjaxUid = {$plugin.tx_eptheme.settings.defaultAjaxUid}
|
||||||
|
settings.mapTileUrl = {$plugin.tx_eptheme.settings.mapTileUrl}
|
||||||
|
settings.mapApiKey = {$plugin.tx_eptheme.settings.mapApiKey}
|
||||||
|
settings.mapAttribution = {$plugin.tx_eptheme.settings.mapAttribution}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,10 @@ module.tx_form.settings.yamlConfigurations {
|
|||||||
1630994710 = EXT:ep_theme/Configuration/Yaml/FormsBase.yaml
|
1630994710 = EXT:ep_theme/Configuration/Yaml/FormsBase.yaml
|
||||||
1630994711 = EXT:ep_theme/Configuration/Yaml/FormFinishersBackend.yaml
|
1630994711 = EXT:ep_theme/Configuration/Yaml/FormFinishersBackend.yaml
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugin.tx_bwcaptcha.settings {
|
||||||
|
charset = abcdefghjkmnpqrtuvwxy34679
|
||||||
|
length = 4
|
||||||
|
width = 220
|
||||||
|
height = 60
|
||||||
|
}
|
||||||
|
|||||||
+12
-2
@@ -1,10 +1,20 @@
|
|||||||
config.tx_replacer {
|
config.tx_replacer {
|
||||||
enable_regex = 1
|
enable_regex = 1
|
||||||
|
|
||||||
|
# The quote character is captured rather than matched literally, because the
|
||||||
|
# @font-face block in Page/Main.typoscript writes its url() values with single
|
||||||
|
# quotes - anchoring on `"` alone would skip every font.
|
||||||
|
# typo3conf is scoped to ep_theme/Resources/Public on purpose: it keeps other
|
||||||
|
# extensions, Configuration/ and Resources/Private/ out of the CDN.
|
||||||
|
# The icon sprite is excluded: SVG <use href="...#icon"> may only reference a
|
||||||
|
# document from the same origin - browsers block an external sprite on the CDN
|
||||||
|
# host outright, and no CORS header on bunny.net changes that. It has to stay
|
||||||
|
# on the origin domain or every icon renders empty.
|
||||||
search {
|
search {
|
||||||
10 = /"\/?(fileadmin|typo3temp|uploads)/
|
10 = /(["'])\/?(fileadmin|typo3temp|uploads|typo3conf\/ext\/ep_theme\/Resources\/Public(?!\/images\/icons_sprite\.svg))/
|
||||||
}
|
}
|
||||||
|
|
||||||
replace {
|
replace {
|
||||||
10 = "https://cdn.ep-reisen.de/$1
|
10 = $1https://cdn.ep-reisen.de/$2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,5 +5,6 @@ plugin.tx_typo3encore {
|
|||||||
builds {
|
builds {
|
||||||
EpThemeConfig = EXT:ep_theme/Resources/Public
|
EpThemeConfig = EXT:ep_theme/Resources/Public
|
||||||
}
|
}
|
||||||
|
preload.enable = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ plugin.tx_eptheme {
|
|||||||
# cat=eptheme/200/350; type=string; label='Corona Updates' page UID
|
# cat=eptheme/200/350; type=string; label='Corona Updates' page UID
|
||||||
coronaUpdatesPageUid =
|
coronaUpdatesPageUid =
|
||||||
|
|
||||||
|
# cat=eptheme/500/100; type=string; label=Map tile URL template (without apiKey)
|
||||||
|
mapTileUrl = https://api-eu.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png
|
||||||
|
# cat=eptheme/500/110; type=string; label=Map tile API key (Geoapify), restricted by referrer
|
||||||
|
mapApiKey =
|
||||||
|
# cat=eptheme/500/120; type=string; label=Map attribution (Geoapify and OpenStreetMap credits are both required)
|
||||||
|
mapAttribution = Powered by <a href="https://www.geoapify.com/">Geoapify</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors
|
||||||
|
|
||||||
# cat=eptheme/400/100; type=string; label=Phone number general
|
# cat=eptheme/400/100; type=string; label=Phone number general
|
||||||
phoneNumber = 0221 - 272 276 0
|
phoneNumber = 0221 - 272 276 0
|
||||||
# cat=eptheme/400/110; type=string; label=Phone number WhatsApp
|
# cat=eptheme/400/110; type=string; label=Phone number WhatsApp
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ TYPO3:
|
|||||||
finishersDefinition:
|
finishersDefinition:
|
||||||
BpnApiFinisher:
|
BpnApiFinisher:
|
||||||
implementationClassName: 'EP\EpTheme\Form\BpnApiFinisher'
|
implementationClassName: 'EP\EpTheme\Form\BpnApiFinisher'
|
||||||
|
ApplicationApiFinisher:
|
||||||
|
implementationClassName: 'EP\EpTheme\Form\ApplicationApiFinisher'
|
||||||
WebhookFinisher:
|
WebhookFinisher:
|
||||||
implementationClassName: 'EP\EpTheme\Form\WebhookFinisher'
|
implementationClassName: 'EP\EpTheme\Form\WebhookFinisher'
|
||||||
AddReferrerFinisher:
|
AddReferrerFinisher:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ TYPO3:
|
|||||||
1000:
|
1000:
|
||||||
value: 'BpnApiFinisher'
|
value: 'BpnApiFinisher'
|
||||||
label: 'BusPro Finisher'
|
label: 'BusPro Finisher'
|
||||||
|
1010:
|
||||||
|
value: 'ApplicationApiFinisher'
|
||||||
|
label: 'Application Finisher'
|
||||||
1050:
|
1050:
|
||||||
value: 'WebhookFinisher'
|
value: 'WebhookFinisher'
|
||||||
label: 'Webhook Finisher'
|
label: 'Webhook Finisher'
|
||||||
@@ -34,6 +37,25 @@ TYPO3:
|
|||||||
propertyPath: 'options.mapping'
|
propertyPath: 'options.mapping'
|
||||||
propertyValidators:
|
propertyValidators:
|
||||||
10: 'NotEmpty'
|
10: 'NotEmpty'
|
||||||
|
1010:
|
||||||
|
identifier: 'ApplicationApiFinisher'
|
||||||
|
editors:
|
||||||
|
__inheritances:
|
||||||
|
10: 'TYPO3.CMS.Form.mixins.formElementMixins.BaseCollectionEditorsMixin'
|
||||||
|
100:
|
||||||
|
label: 'Application Finisher'
|
||||||
|
110:
|
||||||
|
identifier: 'mapping'
|
||||||
|
templateName: 'Inspector-TextEditor'
|
||||||
|
label: 'Mapping'
|
||||||
|
propertyPath: 'options.mapping'
|
||||||
|
propertyValidators:
|
||||||
|
10: 'NotEmpty'
|
||||||
|
120:
|
||||||
|
identifier: 'exclude'
|
||||||
|
templateName: 'Inspector-TextEditor'
|
||||||
|
label: 'Exclude'
|
||||||
|
propertyPath: 'options.exclude'
|
||||||
1050:
|
1050:
|
||||||
identifier: 'WebhookFinisher'
|
identifier: 'WebhookFinisher'
|
||||||
editors:
|
editors:
|
||||||
@@ -70,6 +92,13 @@ TYPO3:
|
|||||||
predefinedDefaults:
|
predefinedDefaults:
|
||||||
options:
|
options:
|
||||||
mapping: 'lastName:text-1,firstName:text-2,gender:singleselect-1,email:email-1,phone:telephone-1'
|
mapping: 'lastName:text-1,firstName:text-2,gender:singleselect-1,email:email-1,phone:telephone-1'
|
||||||
|
ApplicationApiFinisher:
|
||||||
|
formEditor:
|
||||||
|
iconIdentifier: 'form-finisher'
|
||||||
|
predefinedDefaults:
|
||||||
|
options:
|
||||||
|
mapping: 'firstName:text-1,lastName:text-2,email:email-1,phone:telephone-1'
|
||||||
|
exclude: 'captcha,privacypolicyaccepted'
|
||||||
WebhookFinisher:
|
WebhookFinisher:
|
||||||
formEditor:
|
formEditor:
|
||||||
iconIdentifier: 'form-finisher'
|
iconIdentifier: 'form-finisher'
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ TYPO3:
|
|||||||
options:
|
options:
|
||||||
identifier: 'BpnApiFinisher'
|
identifier: 'BpnApiFinisher'
|
||||||
mapping: 'lastName:text-1,firstName:text-2,gender:singleselect-1,email:email-1,phone:telephone-1'
|
mapping: 'lastName:text-1,firstName:text-2,gender:singleselect-1,email:email-1,phone:telephone-1'
|
||||||
|
ApplicationApiFinisher:
|
||||||
|
implementationClassName: 'EP\EpTheme\Form\ApplicationApiFinisher'
|
||||||
|
formEditor:
|
||||||
|
label: 'Application Finisher'
|
||||||
|
options:
|
||||||
|
identifier: 'ApplicationApiFinisher'
|
||||||
|
mapping: 'firstName:text-1,lastName:text-2,email:email-1,phone:telephone-1'
|
||||||
|
exclude: 'captcha,privacypolicyaccepted'
|
||||||
WebhookFinisher:
|
WebhookFinisher:
|
||||||
implementationClassName: 'EP\EpTheme\Form\WebhookFinisher'
|
implementationClassName: 'EP\EpTheme\Form\WebhookFinisher'
|
||||||
formEditor:
|
formEditor:
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// Import dependencies
|
|
||||||
require('lazysizes')
|
|
||||||
require('lazysizes/plugins/unveilhooks/ls.unveilhooks')
|
|
||||||
// require('picturefill')
|
|
||||||
|
|
||||||
// Polyfills
|
// Polyfills
|
||||||
// import smoothscroll from 'smoothscroll-polyfill'
|
// import smoothscroll from 'smoothscroll-polyfill'
|
||||||
// smoothscroll.polyfill()
|
// smoothscroll.polyfill()
|
||||||
|
|||||||
-791
@@ -1,791 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="mb-8"
|
|
||||||
v-show="!submitted">
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
Zeitraum
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
placeholder="von... bis" ref="picker"/>
|
|
||||||
<p class="text-sm mt-2" v-if="mainSeasonFrom && mainSeasonTo">
|
|
||||||
in der Hauptsaison {{ mainSeasonFrom }}-{{ mainSeasonTo }} mind. {{ minBookingDays }} Tage
|
|
||||||
</p>
|
|
||||||
<p class="text-sm italic mt-2"
|
|
||||||
v-show="nightsCount === 1">
|
|
||||||
Einzelne Nächte sind nur auf Anfrage buchbar
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8 grid sm:grid-cols-2 gap-8"
|
|
||||||
v-show="!submitted">
|
|
||||||
<div>
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
Personenzahl
|
|
||||||
</label>
|
|
||||||
<input type="number"
|
|
||||||
class="form-field"
|
|
||||||
min="30"
|
|
||||||
v-model.number="selectedPax"
|
|
||||||
@change="onPaxUpdated()"
|
|
||||||
v-show="nightsCount > 0"/>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount === 0">
|
|
||||||
Bitte zuerst einen Zeitraum wählen
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
davon Kinder 0-3 Jahre
|
|
||||||
</label>
|
|
||||||
<input type="number"
|
|
||||||
class="form-field"
|
|
||||||
min="0"
|
|
||||||
v-model.number="childrenCount"
|
|
||||||
@change="onPaxUpdated()"
|
|
||||||
v-show="nightsCount > 0"/>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount === 0">
|
|
||||||
Bitte zuerst einen Zeitraum wählen
|
|
||||||
</p>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount > 0">
|
|
||||||
Kinder werden nur bei Strom- und Abfallgebühren berücksichtigt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
davon Kinder 4-5 Jahre*
|
|
||||||
</label>
|
|
||||||
<input type="number"
|
|
||||||
class="form-field"
|
|
||||||
min="0"
|
|
||||||
v-model.number="minorsCount"
|
|
||||||
@change="onPaxUpdated()"
|
|
||||||
v-show="nightsCount > 0"/>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount === 0">
|
|
||||||
Bitte zuerst einen Zeitraum wählen
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="adolescentsAge > 0">
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
davon Kinder 6-{{ adolescentsAge }} Jahre*
|
|
||||||
</label>
|
|
||||||
<input type="number"
|
|
||||||
class="form-field"
|
|
||||||
min="0"
|
|
||||||
v-model.number="adolescentsCount"
|
|
||||||
@change="onPaxUpdated()"
|
|
||||||
v-show="nightsCount > 0"/>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount === 0">
|
|
||||||
Bitte zuerst einen Zeitraum wählen
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-2 text-sm">
|
|
||||||
*Die Kosten für Essen und Kurtaxe werden wir entsprechend der Altersstruktur der Gäste in der Rechnung
|
|
||||||
anpassen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8"
|
|
||||||
v-show="options.length > 0 && !submitted">
|
|
||||||
<div class="mb-2 font-bold">
|
|
||||||
Optionale Zusatzleistungen
|
|
||||||
</div>
|
|
||||||
<ul class="mb-0 p-0 space-y-2">
|
|
||||||
<li v-for="option of options" :key="option.uid">
|
|
||||||
<label class="flex items-start">
|
|
||||||
<input type="checkbox"
|
|
||||||
class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
|
||||||
v-model="selectedOptions"
|
|
||||||
:value="option">
|
|
||||||
<span class="pl-2 leading-none">{{ option.title }}{{ formatOptionPriceType(option) }}</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8"
|
|
||||||
v-show="boards.length > 0 && !submitted">
|
|
||||||
<label class="mb-2 font-bold">
|
|
||||||
Verpflegungsleistungen
|
|
||||||
</label>
|
|
||||||
<select class="form-field"
|
|
||||||
v-model="selectedBoard"
|
|
||||||
v-if="selectedPax >= 30">
|
|
||||||
<option :value="null" disabled>bitte auswählen</option>
|
|
||||||
<option v-for="board of boards"
|
|
||||||
:value="board"
|
|
||||||
:key="board.uid">
|
|
||||||
{{ board.title }} ({{ formatCurrency(board.price) }} pro Person und Nacht)
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.board"
|
|
||||||
v-html="formErrors.board"></div>
|
|
||||||
<div v-show="selectedPax < 30">
|
|
||||||
<span class="text-red-600">Erst ab 30 Personen buchbar.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="pb-2" v-if="submitted">
|
|
||||||
<p class="text-lg font-bold pb-2">
|
|
||||||
Vielen Dank für Deine {{ mode === 'booking' ? 'Buchung' : 'Anfrage' }}.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Für folgendes habt ihr euch entschieden:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 uppercase bg-ep-primary-dark text-white w-full">
|
|
||||||
Preise
|
|
||||||
</div>
|
|
||||||
<div class="bg-zinc-100 p-2 mb-2">
|
|
||||||
<table class="w-full" v-show="nightsCount > 0">
|
|
||||||
<tbody>
|
|
||||||
<tr class="odd:bg-zinc-100">
|
|
||||||
<th class="text-left py-2">Zeitraum</th>
|
|
||||||
<td>{{ rangeFormatted }}, {{ nightsCount }} Nächte</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100">
|
|
||||||
<th class="text-left py-2">Personenzahl</th>
|
|
||||||
<td>
|
|
||||||
{{ selectedPax }}<span v-if="childrenCount > 0">, davon Kinder {{ childrenCount }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100">
|
|
||||||
<th class="text-left py-2">Basispreis</th>
|
|
||||||
<td>{{ formatCurrency(pricePax.base) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-if="pricePax.additional.EUR > 0 || pricePax.additional.CHF > 0">
|
|
||||||
<th class="text-left py-2">Aufpreis Personenzahl</th>
|
|
||||||
<td>{{ formatCurrency(pricePax.additional) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-if="priceShortTerm.EUR > 0 || priceShortTerm.CHF > 0">
|
|
||||||
<th class="text-left py-2">Aufpreis Kurzzeit</th>
|
|
||||||
<td>{{ formatCurrency(priceShortTerm) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-for="selectedOption in selectedOptions">
|
|
||||||
<th class="text-left py-2">{{ selectedOption.title }}</th>
|
|
||||||
<td>{{ formatCurrency(calculateOptionPrice(selectedOption)) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-if="priceBoard.EUR > 0 || priceBoard.CHF > 0">
|
|
||||||
<th class="text-left py-2">{{ selectedBoard.title }}</th>
|
|
||||||
<td>{{ formatCurrency(priceBoard) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-if="false === isSelfCatering && undersubscriptionLimit > 0">
|
|
||||||
<th class="text-left py-2">Verpflegungs-Aufschlag für Gruppen unter {{ undersubscriptionLimit }} Personen</th>
|
|
||||||
<td>{{ formatCurrency(pricePax.undersubscription) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100"
|
|
||||||
v-if="priceRunningCosts.EUR > 0 || priceRunningCosts.CHF > 0">
|
|
||||||
<th class="text-left py-2">Strom- und Abfallgebühren</th>
|
|
||||||
<td>{{ formatCurrency(priceRunningCosts) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="odd:bg-zinc-100">
|
|
||||||
<th class="text-left py-2">Gesamtpreis</th>
|
|
||||||
<td>
|
|
||||||
<strong>{{ formatCurrency(priceTotal) }}</strong>
|
|
||||||
<br>
|
|
||||||
<small>{{ taxLabel }}</small>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="text-sm italic"
|
|
||||||
v-show="nightsCount === 0">
|
|
||||||
Bitte zuerst einen Zeitraum wählen
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="mode === 'booking' && submitted">
|
|
||||||
Bitte beachtet, dass die Buchung bei uns eingegangen, aber <strong>noch nicht bestätigt</strong> ist.
|
|
||||||
Wir behalten uns vor, die Buchungen auf Verfügbarkeit und Länge des gewünschten Aufenthalts zu prüfen.
|
|
||||||
Erst wenn wir euch die Buchung per Mail bestätigt haben, wird sie bindend.
|
|
||||||
</div>
|
|
||||||
<div v-if="mode === 'inquiry' && submitted">
|
|
||||||
Die Anfrage ist bei uns eingegangen und wird schnellstmöglich bearbeitet. Wir melden uns telefonisch
|
|
||||||
oder per Mail bei euch.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8"
|
|
||||||
v-show="nightsCount > 0 && !submitted">
|
|
||||||
<div class="p-2 uppercase bg-ep-primary-dark text-white w-full">
|
|
||||||
Jetzt <span v-if="!forceInquiry">buchen</span><span v-if="forceInquiry">anfragen</span>
|
|
||||||
</div>
|
|
||||||
<div class="bg-zinc-100 p-2">
|
|
||||||
<div class="grid md:grid-cols-2 gap-4 mb-4">
|
|
||||||
<div :class="{ 'has-error': formErrors.name }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Name*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="name">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.name"
|
|
||||||
v-html="formErrors.name"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.street }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Strasse, Nr.*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="street">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.street"
|
|
||||||
v-html="formErrors.street"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.postcode }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Postleitzahl*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="postcode">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.postcode"
|
|
||||||
v-html="formErrors.postcode"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.city }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Ort*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="city">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.city"
|
|
||||||
v-html="formErrors.city"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.group }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Name der Gruppe*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="group">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.group"
|
|
||||||
v-html="formErrors.group"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.email }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
E-Mail*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="email">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.email"
|
|
||||||
v-html="formErrors.email"></div>
|
|
||||||
</div>
|
|
||||||
<div :class="{ 'has-error': formErrors.phone }">
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Telefon*
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-field"
|
|
||||||
v-model="phone">
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.phone"
|
|
||||||
v-html="formErrors.phone"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="font-bold mb-2">
|
|
||||||
Bemerkungen/Wünsche
|
|
||||||
</label>
|
|
||||||
<textarea class="form-field"
|
|
||||||
cols="30"
|
|
||||||
rows="3"
|
|
||||||
v-model="remarks"/>
|
|
||||||
</div>
|
|
||||||
<div v-if="mode === 'booking'" :class="{ 'has-error': formErrors.confirmation }">
|
|
||||||
<label for="confirmation" class="flex items-start">
|
|
||||||
<input type="checkbox" v-model="confirmation" name="confirmation" id="confirmation">
|
|
||||||
<span class="block ml-2">
|
|
||||||
Durch Anklicken des Buttons 'Buchung abschicken' bestätige ich, dass ich die
|
|
||||||
<a :href="termsUrls[countryCode]"
|
|
||||||
class="text-ep-primary" target="_blank">
|
|
||||||
Allgemeinen Geschäftsbedingungen (AGB)
|
|
||||||
</a> gelesen habe und damit einverstanden bin, dass eine
|
|
||||||
kostenpflichtige Buchung zustande kommt.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="text-sm text-red-600 mt-1"
|
|
||||||
v-show="formErrors.confirmation"
|
|
||||||
v-html="formErrors.confirmation"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="py-4">
|
|
||||||
Ihr erhaltet von uns eine Bestätigung. Wir behalten uns vor die Buchung zu prüfen. Erst nach
|
|
||||||
Rückbestätigung von uns gilt die Buchung auch von unserer Seite als bindend.
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<button class="button bg-button"
|
|
||||||
type="submit"
|
|
||||||
v-if="!forceInquiry"
|
|
||||||
@click.prevent="submitForm('booking')">
|
|
||||||
Buchung abschicken
|
|
||||||
</button>
|
|
||||||
<button class="button bg-button"
|
|
||||||
:class="{ 'button--small bg-ep-secondary': !forceInquiry }"
|
|
||||||
type="submit"
|
|
||||||
@click.prevent="submitForm('inquiry')">
|
|
||||||
als Anfrage senden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<img :src="logoAt" alt="" v-if="countryCode === 'AT'" class="block w-full h-auto max-w-24">
|
|
||||||
<img :src="logoCh" alt="" v-if="countryCode === 'CH'" class="block w-full h-auto max-w-24">
|
|
||||||
</div>
|
|
||||||
<div class="fixed top-0 left-0 inset-0 bg-white/85 flex items-center justify-center" v-show="processing">
|
|
||||||
<img :src="loaderUri" alt="Loading...">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import 'dayjs/locale/de'
|
|
||||||
import 'flatpickr';
|
|
||||||
import {German} from 'flatpickr/dist/l10n/de';
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'GroupsPriceCalculator',
|
|
||||||
props: {
|
|
||||||
loaderUri: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
endpoint: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
logoAt: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
logoCh: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
configsJson: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
boardsJson: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
optionsJson: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
runningCostsFactorEur: {
|
|
||||||
type: Number,
|
|
||||||
default: 2.5,
|
|
||||||
},
|
|
||||||
runningCostsFactorChf: {
|
|
||||||
type: Number,
|
|
||||||
default: 2.9,
|
|
||||||
},
|
|
||||||
undersubscription30Eur: {
|
|
||||||
type: Number,
|
|
||||||
default: 5.0,
|
|
||||||
},
|
|
||||||
undersubscription30Chf: {
|
|
||||||
type: Number,
|
|
||||||
default: 5.0,
|
|
||||||
},
|
|
||||||
undersubscription40Eur: {
|
|
||||||
type: Number,
|
|
||||||
default: 2.5,
|
|
||||||
},
|
|
||||||
undersubscription40Chf: {
|
|
||||||
type: Number,
|
|
||||||
default: 2.5,
|
|
||||||
},
|
|
||||||
countryCode: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
shortTermFactors: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return [
|
|
||||||
{ nights: 1, factor: 0.3 },
|
|
||||||
{ nights: 2, factor: 0.2 },
|
|
||||||
{ nights: 3, factor: 0.1 },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mainSeasonFrom: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
mainSeasonTo: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
minBookingDays: {
|
|
||||||
type: Number,
|
|
||||||
default: 5,
|
|
||||||
},
|
|
||||||
taxLabel: {
|
|
||||||
type: String,
|
|
||||||
default: 'zzgl. vorab zu entrichtender Ortstaxe',
|
|
||||||
},
|
|
||||||
adolescentsAge: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
configs: JSON.parse(this.configsJson),
|
|
||||||
boards: JSON.parse(this.boardsJson),
|
|
||||||
options: JSON.parse(this.optionsJson),
|
|
||||||
processing: false,
|
|
||||||
submitted: false,
|
|
||||||
picker: null,
|
|
||||||
formErrors: {},
|
|
||||||
selectedFrom: null,
|
|
||||||
selectedTo: null,
|
|
||||||
selectedRange: [],
|
|
||||||
selectedPax: 30,
|
|
||||||
childrenCount: 0,
|
|
||||||
minorsCount: 0,
|
|
||||||
adolescentsCount: 0,
|
|
||||||
selectedOptions: [],
|
|
||||||
selectedBoard: null,
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
remarks: '',
|
|
||||||
group: '',
|
|
||||||
street: '',
|
|
||||||
postcode: '',
|
|
||||||
city: '',
|
|
||||||
mode: 'booking',
|
|
||||||
confirmation: false,
|
|
||||||
termsUrls: {
|
|
||||||
AT: 'https://www.ep-reisen.de/fileadmin/user_upload/allgemein/AGB_Gruppen/AGB_Gruppen_CLLT_Touristik_GmbH.pdf',
|
|
||||||
CH: 'https://www.ep-reisen.de/fileadmin/user_upload/allgemein/AGB_Gruppen/AVB_Gruppen_AlpineVacation_GmbH.pdf',
|
|
||||||
IT: 'https://www.ep-reisen.de/fileadmin/user_upload/allgemein/AGB_Gruppen/AGB_Gruppen_E_P_Reisen.pdf',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatCurrency({ EUR, CHF }) {
|
|
||||||
let price
|
|
||||||
|
|
||||||
if ('CH' === this.countryCode) {
|
|
||||||
let formatter = new Intl.NumberFormat('de-DE', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'CHF',
|
|
||||||
})
|
|
||||||
price = formatter.format(CHF)
|
|
||||||
} else {
|
|
||||||
let formatter = new Intl.NumberFormat('de-DE', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'EUR',
|
|
||||||
})
|
|
||||||
price = formatter.format(EUR)
|
|
||||||
}
|
|
||||||
|
|
||||||
return price
|
|
||||||
},
|
|
||||||
calculateOptionPrice(option) {
|
|
||||||
let price = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
};
|
|
||||||
let optionPriceEUR = option.price.EUR
|
|
||||||
let optionPriceCHF = option.price.CHF
|
|
||||||
if (1 === option.type) {
|
|
||||||
price.EUR = optionPriceEUR;
|
|
||||||
price.CHF = optionPriceCHF;
|
|
||||||
} else if (2 === option.type) {
|
|
||||||
price.EUR = optionPriceEUR * this.paxCount;
|
|
||||||
price.CHF = optionPriceCHF * this.paxCount;
|
|
||||||
} else if (3 === option.type) {
|
|
||||||
price.EUR = optionPriceEUR * this.nightsCount;
|
|
||||||
price.CHF = optionPriceCHF * this.nightsCount;
|
|
||||||
} else if (4 === option.type) {
|
|
||||||
price.EUR = optionPriceEUR * this.nightsCount * this.paxCount;
|
|
||||||
price.CHF = optionPriceCHF * this.nightsCount * this.paxCount;
|
|
||||||
}
|
|
||||||
return price;
|
|
||||||
},
|
|
||||||
formatOptionPriceType(option) {
|
|
||||||
if (2 === option.type) {
|
|
||||||
return ` (${this.formatCurrency(option.price)} pro Person)`
|
|
||||||
}
|
|
||||||
if (3 === option.type) {
|
|
||||||
return ` (${this.formatCurrency(option.price)} pro Nacht)`
|
|
||||||
}
|
|
||||||
if (4 === option.type) {
|
|
||||||
return ` (${this.formatCurrency(option.price)} pro Person und Nacht)`
|
|
||||||
}
|
|
||||||
|
|
||||||
return ` (${this.formatCurrency(option.price)})`
|
|
||||||
},
|
|
||||||
initSelectableRanges() {
|
|
||||||
let enabledDates = [];
|
|
||||||
for (let config of this.configs) {
|
|
||||||
const dateFrom = dayjs(config.dateFrom);
|
|
||||||
const dateTo = dayjs(config.dateTo);
|
|
||||||
enabledDates.push({
|
|
||||||
from: dateFrom.format('DD.MM.YYYY'),
|
|
||||||
to: dateTo.format('DD.MM.YYYY')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.picker.set('enable', enabledDates);
|
|
||||||
if (enabledDates.length > 0) {
|
|
||||||
const firstDate = enabledDates[0].from;
|
|
||||||
this.picker.set('minDate', firstDate);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSelectedRange() {
|
|
||||||
let range = [];
|
|
||||||
let currentDate = this.selectedFrom;
|
|
||||||
while (currentDate < this.selectedTo) {
|
|
||||||
range.push(currentDate);
|
|
||||||
currentDate = currentDate.add(1, 'day');
|
|
||||||
}
|
|
||||||
this.selectedRange = range;
|
|
||||||
},
|
|
||||||
onPaxUpdated() {
|
|
||||||
if (this.selectedPax < 30) {
|
|
||||||
this.selectedBoard = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitForm(mode) {
|
|
||||||
this.mode = mode
|
|
||||||
let confirmation = 'inquiry' === mode ? true : this.confirmation
|
|
||||||
let formData = new URLSearchParams({
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][mode]': mode,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][confirmation]': confirmation ? '1' : '0',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][name]': this.name ? this.name : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][group]': this.group ? this.group : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][street]': this.street ? this.street : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][postcode]': this.postcode ? this.postcode : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][city]': this.city ? this.city : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][email]': this.email? this.email : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][phone]': this.phone ? this.phone : '',
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][remarks]': this.remarks,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][dateFrom]': this.selectedFrom.format('DD.MM.YYYY'),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][dateTo]': this.selectedTo.format('DD.MM.YYYY'),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][nights]': this.nightsCount,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][pax]': this.selectedPax,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][children]': this.childrenCount,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][minors]': this.minorsCount,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][adolescents]': this.adolescentsCount,
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][paxBase]': this.formatCurrency(this.pricePax.base),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][paxAdditional]': this.formatCurrency(this.pricePax.additional),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][undersubscription]': this.formatCurrency(this.pricePax.undersubscription),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][shortTerm]': this.formatCurrency(this.priceShortTerm),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][options]': this.formatCurrency(this.priceOptions),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][board]': this.formatCurrency(this.priceBoard),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][runningCosts]': this.formatCurrency(this.priceRunningCosts),
|
|
||||||
'tx_epproducts_ajax[groupsPriceInquiry][summary][total]': this.formatCurrency(this.priceTotal),
|
|
||||||
})
|
|
||||||
if (this.selectedBoard) {
|
|
||||||
formData.append('tx_epproducts_ajax[groupsPriceInquiry][board]', this.selectedBoard.uid)
|
|
||||||
}
|
|
||||||
this.selectedOptions.map(option => (
|
|
||||||
formData.append('tx_epproducts_ajax[groupsPriceInquiry][options][]', option.uid)
|
|
||||||
))
|
|
||||||
this.processing = true;
|
|
||||||
this.submitted = false;
|
|
||||||
fetch(this.endpoint, { method: 'POST', body: formData })
|
|
||||||
.then(response => response.json())
|
|
||||||
.then((json) => {
|
|
||||||
if ('validation' === json.status) {
|
|
||||||
this.formErrors = json.errors
|
|
||||||
} else {
|
|
||||||
this.submitted = true;
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
dataLayer.push({
|
|
||||||
'event': 'Preisberechnung'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.processing = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isSelfCatering() {
|
|
||||||
let selectedBoard = this.selectedBoard
|
|
||||||
if (null === selectedBoard) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ('CH' === this.countryCode) {
|
|
||||||
return selectedBoard.price.CHF === 0
|
|
||||||
} else {
|
|
||||||
return selectedBoard.price.EUR === 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
undersubscriptionLimit() {
|
|
||||||
if (this.paxCount < 40) {
|
|
||||||
return 40
|
|
||||||
} else if (this.paxCount < 50) {
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
paxCount() {
|
|
||||||
// subtract number of children from pax for price calculation, but minimum 30 pax
|
|
||||||
return Math.max(this.selectedPax - this.childrenCount, 30)
|
|
||||||
},
|
|
||||||
nightsCount() {
|
|
||||||
return this.selectedRange.length;
|
|
||||||
},
|
|
||||||
forceInquiry() {
|
|
||||||
return 1 === this.selectedRange.length
|
|
||||||
},
|
|
||||||
rangeFormatted() {
|
|
||||||
if (null === this.selectedFrom || null === this.selectedTo) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.selectedFrom.format('DD.MM.YYYY') + ' - ' + this.selectedTo.format('DD.MM.YYYY');
|
|
||||||
},
|
|
||||||
pricePax() {
|
|
||||||
let base = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
}
|
|
||||||
let additional = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
}
|
|
||||||
let undersubscription = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
}
|
|
||||||
for (let date of this.selectedRange) {
|
|
||||||
for (let config of this.configs) {
|
|
||||||
if (date >= dayjs(config.dateFrom) && date < dayjs(config.dateTo)) {
|
|
||||||
base.EUR += config.price.EUR
|
|
||||||
base.CHF += config.price.CHF
|
|
||||||
let included = config.personsIncluded
|
|
||||||
if (this.paxCount > included) {
|
|
||||||
let additionalPax = this.paxCount - included
|
|
||||||
additional.EUR += additionalPax * config.priceAdditionalPerson.EUR
|
|
||||||
additional.CHF += additionalPax * config.priceAdditionalPerson.CHF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (false === this.isSelfCatering) {
|
|
||||||
if (this.paxCount < 40) {
|
|
||||||
undersubscription.EUR = this.paxCount * this.undersubscription30Eur * this.selectedRange.length
|
|
||||||
undersubscription.CHF = this.paxCount * this.undersubscription30Chf * this.selectedRange.length
|
|
||||||
} else if (this.paxCount < 50) {
|
|
||||||
undersubscription.EUR = this.paxCount * this.undersubscription40Eur * this.selectedRange.length
|
|
||||||
undersubscription.CHF = this.paxCount * this.undersubscription40Chf * this.selectedRange.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {base, additional, undersubscription}
|
|
||||||
},
|
|
||||||
priceOptions() {
|
|
||||||
let price = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
};
|
|
||||||
for (let option of this.selectedOptions) {
|
|
||||||
let optionPrice = this.calculateOptionPrice(option)
|
|
||||||
price.EUR += optionPrice.EUR;
|
|
||||||
price.CHF += optionPrice.CHF;
|
|
||||||
}
|
|
||||||
return price;
|
|
||||||
},
|
|
||||||
priceBoard() {
|
|
||||||
let price = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
}
|
|
||||||
if (this.selectedBoard) {
|
|
||||||
price.EUR = this.selectedBoard.price.EUR * this.nightsCount * this.paxCount;
|
|
||||||
price.CHF = this.selectedBoard.price.CHF * this.nightsCount * this.paxCount;
|
|
||||||
}
|
|
||||||
return price
|
|
||||||
},
|
|
||||||
priceShortTerm() {
|
|
||||||
let price = {
|
|
||||||
EUR: 0,
|
|
||||||
CHF: 0,
|
|
||||||
}
|
|
||||||
const totalPricePax = this.pricePax.base.EUR + this.pricePax.additional.EUR;
|
|
||||||
const totalPricePaxCHF = this.pricePax.base.CHF + this.pricePax.additional.CHF;
|
|
||||||
const shorTermFactor = this.shortTermFactors.find(element => element.nights === this.nightsCount);
|
|
||||||
if (undefined !== shorTermFactor) {
|
|
||||||
price.EUR = totalPricePax * shorTermFactor.factor;
|
|
||||||
price.CHF = totalPricePaxCHF * shorTermFactor.factor;
|
|
||||||
}
|
|
||||||
return price
|
|
||||||
},
|
|
||||||
priceRunningCosts() {
|
|
||||||
return {
|
|
||||||
EUR: this.nightsCount * this.selectedPax * this.runningCostsFactorEur,
|
|
||||||
CHF: this.nightsCount * this.selectedPax * this.runningCostsFactorChf,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
priceTotal() {
|
|
||||||
let totalEUR = this.pricePax.base.EUR
|
|
||||||
+ this.pricePax.additional.EUR
|
|
||||||
+ this.pricePax.undersubscription.EUR
|
|
||||||
+ this.priceOptions.EUR
|
|
||||||
+ this.priceBoard.EUR
|
|
||||||
+ this.priceShortTerm.EUR
|
|
||||||
+ this.priceRunningCosts.EUR
|
|
||||||
|
|
||||||
let totalCHF = 0
|
|
||||||
|
|
||||||
// calculate CHF price when pax price in CHF is available since CHF prices may not be provided via CMS yet
|
|
||||||
if (0 < this.pricePax.base.CHF) {
|
|
||||||
totalCHF = this.pricePax.base.CHF
|
|
||||||
+ this.pricePax.additional.CHF
|
|
||||||
+ this.pricePax.undersubscription.CHF
|
|
||||||
+ this.priceOptions.CHF
|
|
||||||
+ this.priceBoard.CHF
|
|
||||||
+ this.priceShortTerm.CHF
|
|
||||||
+ this.priceRunningCosts.CHF
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
EUR: totalEUR,
|
|
||||||
CHF: totalCHF,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
Vue.nextTick(() => {
|
|
||||||
this.picker = flatpickr(this.$refs.picker, {
|
|
||||||
mode: 'range',
|
|
||||||
dateFormat: 'd.m.Y',
|
|
||||||
locale: German,
|
|
||||||
onChange: selectedDates => {
|
|
||||||
if (2 === selectedDates.length) {
|
|
||||||
this.selectedFrom = dayjs(selectedDates[0]);
|
|
||||||
this.selectedTo = dayjs(selectedDates[1]);
|
|
||||||
this.updateSelectedRange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.initSelectableRanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
+17
-2
@@ -9,6 +9,9 @@ export default class extends Controller {
|
|||||||
zoom: Number,
|
zoom: Number,
|
||||||
externalLink: String,
|
externalLink: String,
|
||||||
markers: Array,
|
markers: Array,
|
||||||
|
tileUrl: String,
|
||||||
|
apiKey: String,
|
||||||
|
attribution: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@@ -44,9 +47,21 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
this.map.fitBounds(bounds)
|
this.map.fitBounds(bounds)
|
||||||
}
|
}
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer(this.tileUrl(), {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
// The site sends Referrer-Policy: same-origin, which strips the Referer on
|
||||||
|
// cross-origin requests. The tile provider needs it to match its key against
|
||||||
|
// the allowed-domain list, so widen the policy for tile requests only.
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
attribution: this.attributionValue
|
||||||
}).addTo(this.map)
|
}).addTo(this.map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tileUrl() {
|
||||||
|
if ('' === this.tileUrlValue || '' === this.apiKeyValue) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const separator = -1 === this.tileUrlValue.indexOf('?') ? '?' : '&'
|
||||||
|
return this.tileUrlValue + separator + 'apiKey=' + encodeURIComponent(this.apiKeyValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// Initialize vue app
|
|
||||||
import Vue from 'vue'
|
|
||||||
import vueCustomElement from 'vue-custom-element'
|
|
||||||
import 'document-register-element/build/document-register-element'
|
|
||||||
import GroupsPriceCalculator from './components/GroupsPriceCalculator'
|
|
||||||
|
|
||||||
Vue.use(vueCustomElement)
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
Vue.customElement('groups-price-calculator', GroupsPriceCalculator)
|
|
||||||
@@ -3,7 +3,3 @@
|
|||||||
@apply block w-full h-auto;
|
@apply block w-full h-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.lazyloading {
|
|
||||||
background: url(../../images/lazy.png) no-repeat top center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
</f:if>
|
</f:if>
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{city.name}" />
|
alt="{city.name}" />
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:section name="Teaser">
|
<f:section name="Teaser">
|
||||||
@@ -18,12 +17,12 @@
|
|||||||
</f:if>
|
</f:if>
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{concept.name}" />
|
alt="{concept.name}" />
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
<div class="col-span-2 md:col-span-1">
|
<div class="col-span-2 md:col-span-1">
|
||||||
<div class="relative max-w-md mx-auto mb-8">
|
<div class="relative max-w-md mx-auto mb-8">
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
height="400c"
|
height="400c"
|
||||||
alt="{image.alternative}" />
|
alt="{image.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
|
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
|
||||||
rel="nofollow"
|
rel="nofollow"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:alias map="{button: data.flexForm.buttons.0}">
|
<f:alias map="{button: data.flexForm.buttons.0}">
|
||||||
@@ -11,11 +10,11 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<a href="{f:uri.typolink(parameter: button.container.link)}" title="{button.container.label -> f:format.raw()}">
|
<a href="{f:uri.typolink(parameter: button.container.link)}" title="{button.container.label -> f:format.raw()}">
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
alt="{image.alternative}" />
|
alt="{image.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
<div class="md:flex md:items-start py-4">
|
<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">
|
<div class="px-16 pb-4 md:px-4 md:pb-0 md:w-1/6">
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
height="400c"
|
height="400c"
|
||||||
alt="{image.alternative}" />
|
alt="{image.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</div>
|
</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="px-16 md:px-4 md:w-5/6 md:pl-8 text-center text-white md:text-left" data-rte-content>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
<div class="col-span-2 md:col-span-1">
|
<div class="col-span-2 md:col-span-1">
|
||||||
<div class="relative max-w-md mx-auto mb-8">
|
<div class="relative max-w-md mx-auto mb-8">
|
||||||
<f:if condition="{teaserImage}">
|
<f:if condition="{teaserImage}">
|
||||||
<ep:lazyLoadImage image="{teaserImage}"
|
<f:image image="{teaserImage}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
height="400c"
|
height="400c"
|
||||||
alt="{teaserImage.alternative}" />
|
alt="{teaserImage.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
|
<a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}"
|
||||||
rel="nofollow"
|
rel="nofollow"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:alias map="{button: data.flexForm.buttons.0}">
|
<f:alias map="{button: data.flexForm.buttons.0}">
|
||||||
@@ -11,11 +10,11 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<a href="{f:uri.typolink(parameter: button.container.link)}" title="{button.container.label -> f:format.raw()}">
|
<a href="{f:uri.typolink(parameter: button.container.link)}" title="{button.container.label -> f:format.raw()}">
|
||||||
<f:if condition="{teaserImage}">
|
<f:if condition="{teaserImage}">
|
||||||
<ep:lazyLoadImage image="{teaserImage}"
|
<f:image image="{teaserImage}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
alt="{teaserImage.alternative}" />
|
alt="{teaserImage.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
<div class="md:flex md:items-start py-4">
|
<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">
|
<div class="px-16 pb-4 md:px-4 md:pb-0 md:w-1/6">
|
||||||
<f:if condition="{teaserImage}">
|
<f:if condition="{teaserImage}">
|
||||||
<ep:lazyLoadImage image="{teaserImage}"
|
<f:image image="{teaserImage}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
height="400c"
|
height="400c"
|
||||||
alt="{teaserImage.alternative}" />
|
alt="{teaserImage.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</div>
|
</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="px-16 md:px-4 md:w-5/6 md:pl-8 text-center text-white md:text-left" data-rte-content>
|
||||||
|
|||||||
@@ -158,11 +158,11 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="nofollow"
|
rel="nofollow"
|
||||||
title="Global Compact">
|
title="Global Compact">
|
||||||
<ep:lazyLoadImage src="EXT:ep_theme/Resources/Public/images/global_compact.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/global_compact.png"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="100"
|
width="100"
|
||||||
alt="Global Compact" />
|
alt="Global Compact" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,8 +184,10 @@
|
|||||||
<f:format.raw>{totalRatings.jsonData->f:format.json()}</f:format.raw>
|
<f:format.raw>{totalRatings.jsonData->f:format.json()}</f:format.raw>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<f:if condition="{site.identifier} == 'ep-reisen'">
|
<f:comment><!--
|
||||||
<f:render partial="Chatbot" arguments="{_all}"/>
|
<f:if condition="{site.identifier} == 'ep-reisen'">
|
||||||
</f:if>
|
<f:render partial="Chatbot" arguments="{_all}"/>
|
||||||
|
</f:if>
|
||||||
|
--></f:comment>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
{ep:icon(icon: 'flag-{hotel.country.code -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6 absolute top-0 left-0')}
|
{ep:icon(icon: 'flag-{hotel.country.code -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6 absolute top-0 left-0')}
|
||||||
<f:if condition="{hotel.images.original.0}">
|
<f:if condition="{hotel.images.original.0}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{hotel.images.original.0}"
|
<f:image image="{hotel.images.original.0}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{hotel.name}" />
|
alt="{hotel.name}" />
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
|
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
|
||||||
title="{image.title}"
|
title="{image.title}"
|
||||||
data-action="lightbox#open">
|
data-action="lightbox#open">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="770"
|
width="770"
|
||||||
height="430c+100"
|
height="430c+100"
|
||||||
alt="{image.alternative}"/>
|
alt="{image.alternative}"/>
|
||||||
</a>
|
</a>
|
||||||
</f:alias>
|
</f:alias>
|
||||||
<f:if condition="{thumbnails}">
|
<f:if condition="{thumbnails}">
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
data-lightbox-caption="{image.originalResource.description}"
|
data-lightbox-caption="{image.originalResource.description}"
|
||||||
title="{image.title}"
|
title="{image.title}"
|
||||||
class="block">
|
class="block">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="150"
|
width="150"
|
||||||
height="85c+100"
|
height="85c+100"
|
||||||
alt="{image.alternative}"/>
|
alt="{image.alternative}"/>
|
||||||
</a>
|
</a>
|
||||||
</f:if>
|
</f:if>
|
||||||
</f:for>
|
</f:for>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<div class="odd:bg-zinc-50 p-4" data-rte-content>
|
<div class="odd:bg-zinc-50 p-4" data-rte-content>
|
||||||
@@ -18,12 +17,12 @@
|
|||||||
</h3>
|
</h3>
|
||||||
{journey.byCar -> f:format.html()}
|
{journey.byCar -> f:format.html()}
|
||||||
<f:if condition="{journey.image}">
|
<f:if condition="{journey.image}">
|
||||||
<ep:lazyLoadImage image="{journey.image}"
|
<f:image image="{journey.image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="770"
|
width="770"
|
||||||
height="430c+100"
|
height="430c+100"
|
||||||
alt="{journey.image.alternative}" />
|
alt="{journey.image.alternative}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</f:if>
|
</f:if>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="w-full aspect-w-16 aspect-h-9 {class}"
|
||||||
|
data-controller="osm"
|
||||||
|
data-osm-tile-url-value="{settings.mapTileUrl}"
|
||||||
|
data-osm-api-key-value="{settings.mapApiKey}"
|
||||||
|
data-osm-attribution-value="{settings.mapAttribution}"
|
||||||
|
<f:if condition="{zoom}">data-osm-zoom-value="{zoom}"</f:if>
|
||||||
|
<f:if condition="{lat}">data-osm-lat-value="{lat}" data-osm-lng-value="{lng}"</f:if>
|
||||||
|
<f:if condition="{externalLink}">data-osm-external-link-value="{externalLink}"</f:if>
|
||||||
|
<f:if condition="{markers}">data-osm-markers-value='{markers -> f:format.raw()}'</f:if>
|
||||||
|
></div>
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
<f:then>
|
<f:then>
|
||||||
<f:if condition="{newsItem.mediaPreviews.0}">
|
<f:if condition="{newsItem.mediaPreviews.0}">
|
||||||
<f:alias map="{mediaElement: '{newsItem.mediaPreviews.0}'}">
|
<f:alias map="{mediaElement: '{newsItem.mediaPreviews.0}'}">
|
||||||
<img class="block w-full h-auto lazyload"
|
<img class="block w-full h-auto"
|
||||||
data-src="{f:uri.image(image: mediaElement, width: '640', height: '360c+50')}"
|
src="{f:uri.image(image: mediaElement, width: '640', height: '360c+50')}"
|
||||||
|
loading="lazy"
|
||||||
alt="{mediaElement.originalResource.alternative}">
|
alt="{mediaElement.originalResource.alternative}">
|
||||||
</f:alias>
|
</f:alias>
|
||||||
</f:if>
|
</f:if>
|
||||||
@@ -19,8 +20,9 @@
|
|||||||
<f:else>
|
<f:else>
|
||||||
<f:if condition="{newsItem.media}">
|
<f:if condition="{newsItem.media}">
|
||||||
<f:alias map="{mediaElement: '{newsItem.media.0}'}">
|
<f:alias map="{mediaElement: '{newsItem.media.0}'}">
|
||||||
<img class="block w-full h-auto lazyload"
|
<img class="block w-full h-auto"
|
||||||
data-src="{f:uri.image(image: mediaElement, width: '640', height: '360c+50')}"
|
src="{f:uri.image(image: mediaElement, width: '640', height: '360c+50')}"
|
||||||
|
loading="lazy"
|
||||||
alt="{mediaElement.originalResource.alternative}">
|
alt="{mediaElement.originalResource.alternative}">
|
||||||
</f:alias>
|
</f:alias>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-1">
|
<div class="col-span-1">
|
||||||
<f:if condition="{officetip.author.image}">
|
<f:if condition="{officetip.author.image}">
|
||||||
<ep:lazyLoadImage image="{officetip.author.image}"
|
<f:image image="{officetip.author.image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="120"
|
width="120"
|
||||||
alt="{officetip.author.name}" />
|
alt="{officetip.author.name}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
|
|||||||
@@ -16,19 +16,19 @@
|
|||||||
<div class="relative{f:if(condition: landscape, then: ' sm:w-2/5')}">
|
<div class="relative{f:if(condition: landscape, then: ' sm:w-2/5')}">
|
||||||
<f:if condition="{productImage}">
|
<f:if condition="{productImage}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{productImage}"
|
<f:image image="{productImage}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{product.name}" />
|
alt="{product.name}" />
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{product.name}" />
|
alt="{product.name}" />
|
||||||
</f:else>
|
</f:else>
|
||||||
</f:if>
|
</f:if>
|
||||||
<div class="absolute top-0 left-0 z-20 inset-x-0 p-2 flex justify-between items-center {f:if(condition: concept, then: 'bg-concept-{concept.code}/70', else: 'bg-ep-primary/80')}">
|
<div class="absolute top-0 left-0 z-20 inset-x-0 p-2 flex justify-between items-center {f:if(condition: concept, then: 'bg-concept-{concept.code}/70', else: 'bg-ep-primary/80')}">
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
</f:if>
|
</f:if>
|
||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{region.name}" />
|
alt="{region.name}" />
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:if condition="{region.webcam.available}">
|
<f:if condition="{region.webcam.available}">
|
||||||
@@ -13,11 +12,11 @@
|
|||||||
data-action="lightbox#open"
|
data-action="lightbox#open"
|
||||||
data-lightbox-caption="Webcam {region.name}"
|
data-lightbox-caption="Webcam {region.name}"
|
||||||
title="Webcam {region.name}">
|
title="Webcam {region.name}">
|
||||||
<ep:lazyLoadImage src="{region.webcam.url}"
|
<f:image src="{region.webcam.url}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="330"
|
width="330"
|
||||||
alt="Webcam {region.name}" />
|
alt="Webcam {region.name}" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:if condition="{region.regionMaps}">
|
<f:if condition="{region.regionMaps}">
|
||||||
@@ -17,11 +16,11 @@
|
|||||||
data-lightbox-index="{iteration.cycle}"
|
data-lightbox-index="{iteration.cycle}"
|
||||||
data-lightbox-caption="Pistenplan {region.name}"
|
data-lightbox-caption="Pistenplan {region.name}"
|
||||||
title="Pistenplan {region.name}">
|
title="Pistenplan {region.name}">
|
||||||
<ep:lazyLoadImage image="{regionMap}"
|
<f:image image="{regionMap}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="330"
|
width="330"
|
||||||
alt="{regionMap.alternative}"/>
|
alt="{regionMap.alternative}"/>
|
||||||
</a>
|
</a>
|
||||||
</f:for>
|
</f:for>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -274,8 +274,12 @@
|
|||||||
2: 'DZ / 2er',
|
2: 'DZ / 2er',
|
||||||
3: '3er',
|
3: '3er',
|
||||||
4: '4er',
|
4: '4er',
|
||||||
5: 'ab 5 Pers.',
|
5: '5er',
|
||||||
6: 'mit anderen'
|
6: '6er',
|
||||||
|
7: '7er',
|
||||||
|
8: '8er',
|
||||||
|
9: '9er und mehr',
|
||||||
|
10: 'mit anderen'
|
||||||
}"/>
|
}"/>
|
||||||
<div class="py-2"
|
<div class="py-2"
|
||||||
data-controller="collapse"
|
data-controller="collapse"
|
||||||
@@ -291,17 +295,19 @@
|
|||||||
<div data-collapse-target="container">
|
<div data-collapse-target="container">
|
||||||
<ul class="mb-0 p-0 pt-4 space-y-2">
|
<ul class="mb-0 p-0 pt-4 space-y-2">
|
||||||
<f:for each="{filterOptions.roomTypes}" as="type">
|
<f:for each="{filterOptions.roomTypes}" as="type">
|
||||||
<li>
|
<f:if condition="{roomTypes.{type}}">
|
||||||
<label class="flex items-start">
|
<li>
|
||||||
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
<label class="flex items-start">
|
||||||
property="roomTypes"
|
<f:form.checkbox class="w-5 h-5 border-ep-primary-light ring-0 bg-transparent focus:ring-0 focus:border-none text-ep-primary"
|
||||||
data="{action: '{stimulusController}#filter'}"
|
property="roomTypes"
|
||||||
value="{type}"/>
|
data="{action: '{stimulusController}#filter'}"
|
||||||
<span class="leading-none pl-2">
|
value="{type}"/>
|
||||||
{roomTypes.{type}}
|
<span class="leading-none pl-2">
|
||||||
</span>
|
{roomTypes.{type}}
|
||||||
</label>
|
</span>
|
||||||
</li>
|
</label>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
</f:for>
|
</f:for>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
|
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
@@ -11,11 +10,11 @@
|
|||||||
Teamer vor Ort
|
Teamer vor Ort
|
||||||
</p>
|
</p>
|
||||||
<f:if condition="{teamerInCharge.image}">
|
<f:if condition="{teamerInCharge.image}">
|
||||||
<ep:lazyLoadImage image="{teamerInCharge.image}"
|
<f:image image="{teamerInCharge.image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto mb-4"
|
class="block w-full h-auto mb-4"
|
||||||
width="330"
|
width="330"
|
||||||
alt="{teamerInCharge.name}"/>
|
alt="{teamerInCharge.name}"/>
|
||||||
</f:if>
|
</f:if>
|
||||||
<p>{teamerInCharge.name}</p>
|
<p>{teamerInCharge.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
<f:if condition="{context.country}">
|
<f:if condition="{context.country}">
|
||||||
{ep:icon(icon: 'flag-{context.country.code -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6 absolute top-0 left-0')}
|
{ep:icon(icon: 'flag-{context.country.code -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6 absolute top-0 left-0')}
|
||||||
</f:if>
|
</f:if>
|
||||||
<ep:lazyLoadImage image="{teaserImage}"
|
<f:image image="{teaserImage}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{teaserImage.alternative}" />
|
alt="{teaserImage.alternative}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col pt-4 bg-white border border-t-0 border-zinc-200">
|
<div class="flex-1 flex flex-col pt-4 bg-white border border-t-0 border-zinc-200">
|
||||||
<a href="{f:uri.typolink(parameter: data.header_link)}" class="px-2 peer headline--3 mb-2 no-underline">
|
<a href="{f:uri.typolink(parameter: data.header_link)}" class="px-2 peer headline--3 mb-2 no-underline">
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:if condition="{image.uid}">
|
<f:if condition="{image.uid}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(min-width: 1201px)" data-srcset="{f:uri.image(
|
<source media="(min-width: 1201px)" srcset="{f:uri.image(
|
||||||
image: image, width: '370', height: '220c-100'
|
image: image, width: '370', height: '220c-100'
|
||||||
)} 370w"/>
|
)} 370w"/>
|
||||||
<source media="(max-width: 1200px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 1200px)" srcset="{f:uri.image(
|
||||||
image: image, width: '320', height: '160c-100'
|
image: image, width: '320', height: '160c-100'
|
||||||
)} 320w"/>
|
)} 320w"/>
|
||||||
<source media="(max-width: 991px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 991px)" srcset="{f:uri.image(
|
||||||
image: image, width: '360', height: '200c-100'
|
image: image, width: '360', height: '200c-100'
|
||||||
)} 360w"/>
|
)} 360w"/>
|
||||||
<source media="(max-width: 768px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 768px)" srcset="{f:uri.image(
|
||||||
image: image, width: '500', height: '250c-100'
|
image: image, width: '500', height: '250c-100'
|
||||||
)} 500w"/>
|
)} 500w"/>
|
||||||
<source media="(max-width: 512px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 512px)" srcset="{f:uri.image(
|
||||||
image: image, width: '350', height: '175c-100'
|
image: image, width: '350', height: '175c-100'
|
||||||
)} 350w"/>
|
)} 350w"/>
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="500"
|
width="500"
|
||||||
height="250c-100"
|
height="250c-100"
|
||||||
alt="{image.alternative}" />
|
alt="{image.alternative}" />
|
||||||
</picture>
|
</picture>
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(min-width: 1201px)" data-srcset="{f:uri.image(
|
<source media="(min-width: 1201px)" srcset="{f:uri.image(
|
||||||
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
||||||
width: '370', height: '220c-100'
|
width: '370', height: '220c-100'
|
||||||
)} 370w"/>
|
)} 370w"/>
|
||||||
<source media="(max-width: 1200px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 1200px)" srcset="{f:uri.image(
|
||||||
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
||||||
width: '320', height: '160c-100'
|
width: '320', height: '160c-100'
|
||||||
)} 420w"/>
|
)} 320w"/>
|
||||||
<source media="(max-width: 991px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 991px)" srcset="{f:uri.image(
|
||||||
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
||||||
width: '360', height: '200c-100'
|
width: '360', height: '200c-100'
|
||||||
)} 360w"/>
|
)} 360w"/>
|
||||||
<source media="(max-width: 768px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 768px)" srcset="{f:uri.image(
|
||||||
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
||||||
width: '500', height: '250c-100'
|
width: '500', height: '250c-100'
|
||||||
)} 500w"/>
|
)} 500w"/>
|
||||||
<source media="(max-width: 512px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 512px)" srcset="{f:uri.image(
|
||||||
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
src: 'EXT:ep_theme/Resources/Public/images/header-winter.jpg',
|
||||||
width: '350', height: '175c-100'
|
width: '350', height: '175c-100'
|
||||||
)} 320w"/>
|
)} 350w"/>
|
||||||
<ep:lazyLoadImage src="EXT:ep_theme/Resources/Public/images/header-winter.jpg"
|
<f:image src="EXT:ep_theme/Resources/Public/images/header-winter.jpg"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="360"
|
width="360"
|
||||||
height="270c-100"
|
height="270c-100"
|
||||||
alt="" />
|
alt="" />
|
||||||
</picture>
|
</picture>
|
||||||
</f:else>
|
</f:else>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<f:if condition="{image}">
|
<f:if condition="{image}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<picture class="product-teaser__image">
|
<picture class="product-teaser__image">
|
||||||
<source media="(min-width: 1201px)" data-srcset="{f:uri.image(
|
<source media="(min-width: 1201px)" srcset="{f:uri.image(
|
||||||
image: image, width: '320', height: '200c-100'
|
image: image, width: '320', height: '200c-100'
|
||||||
)} 320w"/>
|
)} 320w"/>
|
||||||
<source media="(max-width: 1200px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 1200px)" srcset="{f:uri.image(
|
||||||
image: image, width: '220', height: '130c-100'
|
image: image, width: '220', height: '130c-100'
|
||||||
)} 220w"/>
|
)} 220w"/>
|
||||||
<source media="(max-width: 991px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 991px)" srcset="{f:uri.image(
|
||||||
image: image, width: '260', height: '220c-100'
|
image: image, width: '260', height: '220c-100'
|
||||||
)} 260w"/>
|
)} 260w"/>
|
||||||
<source media="(max-width: 768px)" data-srcset="{f:uri.image(
|
<source media="(max-width: 768px)" srcset="{f:uri.image(
|
||||||
image: image, width: '320', height: '140c-100'
|
image: image, width: '320', height: '140c-100'
|
||||||
)} 320w"/>
|
)} 320w"/>
|
||||||
<img class="scale lazyload" alt="{image.alternative}"
|
<img class="scale" alt="{image.alternative}"
|
||||||
src="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/lazy.png', width: '260', height: '160')}"
|
src="{f:uri.image(image: image, width: '260', height: '160c-100')}"
|
||||||
data-src="{f:uri.image(image: image, width: '260', height: '160c-100')}"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
</picture>
|
</picture>
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<img class="scale" data-src="https://placehold.it/400x300" alt="">
|
<img class="scale" alt=""
|
||||||
|
src="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/lazy.png', width: '260', height: '160')}"
|
||||||
|
loading="lazy">
|
||||||
</f:else>
|
</f:else>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
{data.bodytext -> f:format.html()}
|
{data.bodytext -> f:format.html()}
|
||||||
<f:if condition="{files}">
|
<f:if condition="{files}">
|
||||||
<f:link.typolink parameter="{files.0.link}" title="{files.0.originalResource.title}">
|
<f:link.typolink parameter="{files.0.link}" title="{files.0.originalResource.title}">
|
||||||
<img class="img-responsive lazyload"
|
<img class="img-responsive"
|
||||||
data-src="{f:uri.image(image: files.0, maxWidth: '770')}"
|
src="{f:uri.image(image: files.0, maxWidth: '770')}"
|
||||||
|
loading="lazy"
|
||||||
alt="{files.0.originalResource.alternative}">
|
alt="{files.0.originalResource.alternative}">
|
||||||
</f:link.typolink>
|
</f:link.typolink>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
|||||||
+6
-7
@@ -1,7 +1,6 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
|
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:section name="Content">
|
<f:section name="Content">
|
||||||
@@ -25,12 +24,12 @@
|
|||||||
<f:section name="Image">
|
<f:section name="Image">
|
||||||
<div class="{cssClasses}">
|
<div class="{cssClasses}">
|
||||||
<f:link.typolink parameter="{image.link}">
|
<f:link.typolink parameter="{image.link}">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="400"
|
height="400"
|
||||||
alt="{image.alternative}" />
|
alt="{image.alternative}" />
|
||||||
</f:link.typolink>
|
</f:link.typolink>
|
||||||
</div>
|
</div>
|
||||||
</f:section>
|
</f:section>
|
||||||
|
|||||||
+1
-5
@@ -13,11 +13,7 @@
|
|||||||
<f:variable name="latitude" value="{region.0.data.latitude}"/>
|
<f:variable name="latitude" value="{region.0.data.latitude}"/>
|
||||||
<f:variable name="longitude" value="{region.0.data.longitude}"/>
|
<f:variable name="longitude" value="{region.0.data.longitude}"/>
|
||||||
</f:if>
|
</f:if>
|
||||||
<div class="w-full aspect-w-16 aspect-h-9 mb-8"
|
<f:render partial="Map/Osm" arguments="{settings: settings, class: 'mb-8', zoom: 17, lat: latitude, lng: longitude}"/>
|
||||||
data-controller="osm"
|
|
||||||
data-osm-lat-value="{latitude}"
|
|
||||||
data-osm-lng-value="{longitude}"
|
|
||||||
data-osm-zoom-value="17"></div>
|
|
||||||
</f:section>
|
</f:section>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-6
@@ -10,12 +10,11 @@
|
|||||||
<f:for each="{items}" as="item" iteration="iteration">
|
<f:for each="{items}" as="item" iteration="iteration">
|
||||||
<div class="lg:flex odd:flex-row-reverse justify-between items-end mb-16">
|
<div class="lg:flex odd:flex-row-reverse justify-between items-end mb-16">
|
||||||
<div class="mb-4 lg:mb-0 lg:w-1/2">
|
<div class="mb-4 lg:mb-0 lg:w-1/2">
|
||||||
<ep:lazyLoadImage image="{item.image.0}"
|
<f:image image="{item.image.0}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="768"
|
width="768"
|
||||||
alt="{item.data.name}"
|
alt="{item.data.name}"/>
|
||||||
title="{item.data.name}" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-1/2">
|
<div class="lg:w-1/2">
|
||||||
<div class="px-4 {f:if(condition: iteration.isEven, then: 'lg:pl-12', else: 'lg:pr-12')}">
|
<div class="px-4 {f:if(condition: iteration.isEven, then: 'lg:pl-12', else: 'lg:pr-12')}">
|
||||||
|
|||||||
+6
-7
@@ -1,6 +1,5 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
|
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||||
|
|
||||||
<f:layout name="Bare"/>
|
<f:layout name="Bare"/>
|
||||||
@@ -12,12 +11,12 @@
|
|||||||
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-flag-{country.code -> f:format.case(mode: 'lower')}"></use>
|
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-flag-{country.code -> f:format.case(mode: 'lower')}"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<f:if condition="{country.teaserImages}">
|
<f:if condition="{country.teaserImages}">
|
||||||
<ep:lazyLoadImage image="{country.teaserImages.0}"
|
<f:image image="{country.teaserImages.0}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-full object-cover"
|
class="block w-full h-full object-cover"
|
||||||
width="400c"
|
width="400c"
|
||||||
height="300c"
|
height="300c"
|
||||||
alt="{country.name}" />
|
alt="{country.name}" />
|
||||||
</f:if>
|
</f:if>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col pt-4 bg-white border border-t-0 border-zinc-200">
|
<div class="flex-1 flex flex-col pt-4 bg-white border border-t-0 border-zinc-200">
|
||||||
|
|||||||
@@ -26,11 +26,7 @@
|
|||||||
<div class="bg-zinc-50 p-4">
|
<div class="bg-zinc-50 p-4">
|
||||||
<p><strong>Lage</strong></p>
|
<p><strong>Lage</strong></p>
|
||||||
<div>
|
<div>
|
||||||
<div class="w-full aspect-w-16 aspect-h-9"
|
<f:render partial="Map/Osm" arguments="{settings: settings, lat: hotel.latitude, lng: hotel.longitude, externalLink: '{f:uri.typolink(parameter: hotel.externalLink)}'}"/>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
<f:section name="Main">
|
<f:section name="Main">
|
||||||
<f:if condition="{markerData}">
|
<f:if condition="{markerData}">
|
||||||
<ep:container data="{data}">
|
<ep:container data="{data}">
|
||||||
<div class="w-full aspect-w-16 aspect-h-9 mb-8"
|
<f:render partial="Map/Osm" arguments="{settings: settings, class: 'mb-8', markers: markerData}"/>
|
||||||
data-controller="osm"
|
|
||||||
data-osm-markers-value='{markerData -> f:format.raw()}'></div>
|
|
||||||
</ep:container>
|
</ep:container>
|
||||||
</f:if>
|
</f:if>
|
||||||
</f:section>
|
</f:section>
|
||||||
|
|||||||
@@ -182,12 +182,11 @@
|
|||||||
title="360°-Rundgang {hotel.name}">
|
title="360°-Rundgang {hotel.name}">
|
||||||
<f:if condition="{product.tourThumbnail}">
|
<f:if condition="{product.tourThumbnail}">
|
||||||
<f:then>
|
<f:then>
|
||||||
<ep:lazyLoadImage image="{product.tourThumbnail}"
|
<f:image image="{product.tourThumbnail}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="330"
|
width="330"
|
||||||
alt="360°-Rundgang {hotel.name}"
|
alt="360°-Rundgang {hotel.name}"/>
|
||||||
title="360°-Rundgang {hotel.name}"/>
|
|
||||||
</f:then>
|
</f:then>
|
||||||
<f:else>
|
<f:else>
|
||||||
<div class="button bg-button w-full">
|
<div class="button bg-button w-full">
|
||||||
@@ -217,12 +216,7 @@
|
|||||||
<p class="font-bold mb-2">
|
<p class="font-bold mb-2">
|
||||||
Lage
|
Lage
|
||||||
</p>
|
</p>
|
||||||
<div class="w-full aspect-w-16 aspect-h-9"
|
<f:render partial="Map/Osm" arguments="{settings: settings, zoom: 15, lat: hotel.latitude, lng: hotel.longitude, externalLink: '{f:uri.typolink(parameter: hotel.externalLink)}'}"/>
|
||||||
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>
|
</div>
|
||||||
<f:render partial="Region/Webcam" arguments="{region: product.region}"/>
|
<f:render partial="Region/Webcam" arguments="{region: product.region}"/>
|
||||||
<f:render partial="RegionMaps" arguments="{region: product.region}"/>
|
<f:render partial="RegionMaps" arguments="{region: product.region}"/>
|
||||||
@@ -335,11 +329,11 @@
|
|||||||
data-lightbox-caption="{image.originalResource.description}"
|
data-lightbox-caption="{image.originalResource.description}"
|
||||||
data-action="lightbox#open"
|
data-action="lightbox#open"
|
||||||
class="{f:if(condition: iteration.isFirst, then: 'col-span-4')}">
|
class="{f:if(condition: iteration.isFirst, then: 'col-span-4')}">
|
||||||
<ep:lazyLoadImage image="{image}"
|
<f:image image="{image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto"
|
class="block w-full h-auto"
|
||||||
width="{f:if(condition: iteration.isFirst, then: '400', else: '160')}"
|
width="{f:if(condition: iteration.isFirst, then: '400', else: '160')}"
|
||||||
height="{f:if(condition: iteration.isFirst, then: '225c+100', else: '160c+100')}"/>
|
height="{f:if(condition: iteration.isFirst, then: '225c+100', else: '160c+100')}"/>
|
||||||
</a>
|
</a>
|
||||||
</f:for>
|
</f:for>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<html data-namespace-typo3-fluid="true" lang="en"
|
<html data-namespace-typo3-fluid="true" lang="en"
|
||||||
xmlns="http://www.w3.org/1999/xhtml"
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
|
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:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers">
|
||||||
|
|
||||||
<f:layout name="Default"/>
|
<f:layout name="Default"/>
|
||||||
@@ -28,13 +27,12 @@
|
|||||||
arguments: '{member: member}'
|
arguments: '{member: member}'
|
||||||
)}">
|
)}">
|
||||||
<f:if condition="{member.image}">
|
<f:if condition="{member.image}">
|
||||||
<ep:lazyLoadImage image="{member.image}"
|
<f:image image="{member.image}"
|
||||||
loaderimg="EXT:ep_theme/Resources/Public/images/lazy.png"
|
loading="lazy"
|
||||||
class="block w-full h-auto rounded-full"
|
class="block w-full h-auto rounded-full"
|
||||||
width="400"
|
width="400"
|
||||||
height="400c"
|
height="400c"
|
||||||
alt="{member.name}"
|
alt="{member.name}"/>
|
||||||
title="{member.name}" />
|
|
||||||
</f:if>
|
</f:if>
|
||||||
<div class="text-center text-zinc-800 pt-2">
|
<div class="text-center text-zinc-800 pt-2">
|
||||||
<span class="block font-bold text-xl">
|
<span class="block font-bold text-xl">
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -9,7 +9,6 @@ Encore
|
|||||||
.setOutputPath('./public/typo3conf/ext/ep_theme/Resources/Public/')
|
.setOutputPath('./public/typo3conf/ext/ep_theme/Resources/Public/')
|
||||||
.setPublicPath('/typo3conf/ext/ep_theme/Resources/Public/')
|
.setPublicPath('/typo3conf/ext/ep_theme/Resources/Public/')
|
||||||
.addEntry('main', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/main.js')
|
.addEntry('main', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/main.js')
|
||||||
.addEntry('groups-calculator', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/groups-calculator.js')
|
|
||||||
.addStyleEntry('rte', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/rte.scss')
|
.addStyleEntry('rte', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/rte.scss')
|
||||||
.splitEntryChunks()
|
.splitEntryChunks()
|
||||||
.enableSingleRuntimeChunk()
|
.enableSingleRuntimeChunk()
|
||||||
|
|||||||
Reference in New Issue
Block a user