Compare commits

..
29 Commits
Author SHA1 Message Date
fromme 49547ecda3 fix: reuse the MyEP api connection instead of a client per call 2026-09-22 16:49:44 +02:00
fromme eb096bd8b5 chore: update project dependencies 2026-09-22 15:49:34 +02:00
fromme b3541b21fb feat: modernize fe tooling and drop weight 2026-09-22 15:29:34 +02:00
fromme 93ed5c6962 fix: harden encoded filter settings against malformed and stale input 2026-09-22 14:49:04 +02:00
fromme 942724fdd1 feat: derive room type from occupancy instead of a mapping table 2026-09-22 14:47:04 +02:00
frommeandClaude Opus 5 e52f7dcff8 feat: post application forms to MyEP with label-keyed extra values
Forms built in the backend form editor use generated element identifiers
(text-1, multicheckbox-2, ...) that carry no meaning and cannot be renamed
without breaking the other finishers configured on the form. ApplicationApiFinisher
therefore sends two halves: a small mapped core (firstName, lastName, email, ...)
that a developer configures per form and MyEP can rely on, plus every remaining
submitted value keyed by the element label for a human to read.

Each submission also carries where it came from: the form identifier and its
editor-given name, plus the uid and canonical URL of the page it was submitted
from - the same definition is embedded on several pages, so the page is what
distinguishes them. The URL is generated through the site router in the visitor's
language, like AddReferrerFinisher does it.

Labels come from the form definition rather than TranslationService, so the keys
do not shift with the site language. Values are normalized for JSON: dates to
Y-m-d, uploads to the file name, multi-value elements to a list, empty values
dropped, unsupported objects skipped with a log warning.

Like BpnApiFinisher the request failure is only logged, so a MyEP outage never
costs the application mail.

The endpoint (POST applications) does not exist in MyEP yet; the contract handed
over to that team lives in .ddev/plans/MYEP-APPLICATION-ENDPOINT.md. Until it is
built the finisher is registered but attached to no form.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 13:42:07 +02:00
fromme 30e531497d fix: default Geoapify base url to Europe 2026-09-22 10:54:41 +02:00
fromme 2cb37e760a fix: serve svg sprite locally for same-origin restrictions 2026-09-22 10:35:58 +02:00
fromme 3ff3251419 feat: integrate with Geaoapify for maps tiles 2026-09-22 10:25:20 +02:00
fromme abcd39107b fix: serve css/js via cdn 2026-09-22 09:32:15 +02:00
fromme 599e3c7ee2 fix: accept 409 from the MyEP contact form endpoint 2026-09-22 08:29:58 +02:00
fromme 6ff308015a chore: optimize deployer config 2026-09-15 11:38:56 +02:00
fromme 6e0479bf22 fix: reduce timeout values for snowreport import 2026-09-15 11:28:14 +02:00
fromme fb8fddf669 fix: make snowreport import type-safe and drop wget shell-out 2026-09-15 11:14:33 +02:00
fromme f745c7b683 chore: exclude temp dir from repository 2026-09-15 11:08:49 +02:00
fromme 9b3550ec40 feat: migrate to native lazy loading of images 2026-09-12 12:08:09 +02:00
fromme d9943619f6 chore: cleanup 2026-09-12 12:04:52 +02:00
fromme e33b57c9f1 feat: disable chatbot in page footer 2026-09-12 12:04:33 +02:00
fromme dc594efee1 chore: remove obsolete vue component 2026-09-12 11:52:46 +02:00
fromme 432d8da350 chore: update redis config 2026-09-12 11:31:29 +02:00
fromme 5b338eeab6 chore: align redis config with prod 2026-09-12 11:27:08 +02:00
fromme 05ceae5100 fix: simplify generated captchas to improve ux 2026-09-12 11:22:49 +02:00
fromme f005272242 chore: align php version with prod 2026-09-10 13:51:37 +02:00
fromme 7d856d1031 chore: align mariadb version with prod 2026-09-10 13:50:26 +02:00
fromme f9e94231a7 chore: update project dependencies 2026-09-10 12:54:35 +02:00
fromme 0a3e8f59cb fix: repair broken backend search and improve copy traceability 2026-09-10 12:52:55 +02:00
fromme a01d1ba0c5 chore: drop dead description column from product table 2026-09-10 11:50:38 +02:00
fromme 6828e49663 fix: declare importer-written date columns as passthrough 2026-09-10 11:50:38 +02:00
fromme 46001a02ed fix: add TCA default 0 to relation selects for MySQL strict mode 2026-09-10 10:17:10 +02:00
160 changed files with 3608 additions and 7004 deletions
+79 -42
View File
@@ -1,9 +1,8 @@
name: ep-reisen
type: typo3 type: typo3
docroot: public docroot: public
php_version: "7.4" php_version: "7.4"
webserver_type: nginx-fpm webserver_type: nginx-fpm
router_http_port: "80"
router_https_port: "443"
xdebug_enabled: false xdebug_enabled: false
additional_hostnames: additional_hostnames:
- ep-events - ep-events
@@ -16,36 +15,58 @@ additional_hostnames:
additional_fqdns: [] additional_fqdns: []
database: database:
type: mariadb type: mariadb
version: "10.2" version: "11.4"
hooks:
post-start:
- exec: .ddev/claude-code/install.sh
- exec: .ddev/codex-cli/install.sh
webimage_extra_packages: [ripgrep, bubblewrap, socat]
use_dns_when_possible: true use_dns_when_possible: true
timezone: Europe/Berlin timezone: Europe/Berlin
composer_version: "2" composer_version: "2"
web_environment: web_environment:
- TYPO3_CONTEXT=Development - TYPO3_CONTEXT=Development
nodejs_version: "18" nodejs_version: "24"
corepack_enable: false
omit_containers: [ddev-ssh-agent]
# Key features of DDEV's config.yaml: # Key features of DDEV's config.yaml:
# name: <projectname> # Name of the project, automatically provides # name: <projectname> # Name of the project, automatically provides
# http://projectname.ddev.site and https://projectname.ddev.site # http://projectname.ddev.site and https://projectname.ddev.site
# If the name is omitted, the project will take the name of the enclosing directory,
# which is useful if you want to have a copy of the project side by side with this one.
# type: <projecttype> # drupal6/7/8, backdrop, typo3, wordpress, php # type: <projecttype> # asterios, backdrop, cakephp, codeigniter, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, drupal12, generic, joomla, laravel, magento, magento2, maho, modx, php, shopware6, silverstripe, symfony, typo3, wordpress, wp-bedrock
# See https://docs.ddev.com/en/stable/users/quickstart/ for more
# information on the different project types
# docroot: <relative_path> # Relative path to the directory containing index.php. # docroot: <relative_path> # Relative path to the directory containing index.php.
# php_version: "8.1" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3" # php_version: "8.4" # PHP version to use, "5.6" through "8.5"
# You can explicitly specify the webimage but this # You can explicitly specify the webimage but this
# is not recommended, as the images are often closely tied to DDEV's' behavior, # is not recommended, as the images are often closely tied to DDEV's behavior,
# so this can break upgrades. # so this can break upgrades.
# webimage: <docker_image> # nginx/php docker image. # webimage: <docker_image>
# Its unusual to change this option, and we dont recommend it without Docker experience and a good reason.
# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.*
# database: # database:
# type: <dbtype> # mysql, mariadb, postgres # type: <dbtype> # mysql, mariadb, postgres
# version: <version> # database version, like "10.4" or "8.0" # version: <version> # database version, like "10.11" or "8.0"
# MariaDB versions can be 5.5-10.8 and 10.11, MySQL versions can be 5.5-8.0 # MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8, 12.3
# PostgreSQL versions can be 9-16. # MySQL versions can be 5.5-8.0, 8.4, 9.7
# PostgreSQL versions can be 9-18
# You can explicitly specify the dbimage but this
# is not recommended, as the images are often closely tied to DDEV's behavior,
# so this can break upgrades.
# dbimage: <docker_image>
# Its unusual to change this option, and we dont recommend it without Docker experience and a good reason.
# Typically, this means additions to the existing db image using a .ddev/db-build/Dockerfile.*
# router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80) # router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80)
# router_https_port: <port> # Port for https (defaults to global configuration, usually 443) # router_https_port: <port> # Port for https (defaults to global configuration, usually 443)
@@ -55,14 +76,24 @@ nodejs_version: "18"
# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, # "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better,
# as leaving Xdebug enabled all the time is a big performance hit. # as leaving Xdebug enabled all the time is a big performance hit.
# xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart" # xhgui_http_port: "8143"
# Note that for most people the commands # xhgui_https_port: "8142"
# "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better, # The XHGui ports can be changed from the default 8143 and 8142
# as leaving Xhprof enabled all the time is a big performance hit. # Very rarely used
# webserver_type: nginx-fpm, apache-fpm, or nginx-gunicorn # host_xhgui_port: "8142"
# Can be used to change the host binding port of the XHGui
# application. Rarely used; only when port conflict and
# bind_all_ports is used (normally with router disabled)
# xhprof_mode: [prepend|xhgui|global]
# Default is "xhgui"
# webserver_type: nginx-fpm, apache-fpm, generic
# timezone: Europe/Berlin # timezone: Europe/Berlin
# If timezone is unset, DDEV will attempt to derive it from the host system timezone
# using the $TZ environment variable or the /etc/localtime symlink.
# This is the timezone used in the containers and by PHP; # This is the timezone used in the containers and by PHP;
# it can be set to any valid timezone, # it can be set to any valid timezone,
# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
@@ -74,22 +105,27 @@ nodejs_version: "18"
# commands are executed. # commands are executed.
# composer_version: "2" # composer_version: "2"
# You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1 # You can set it to "" or "2" (default) for Composer v2
# to use the latest major version available at the time your container is built. # to use the latest major version available at the time your container is built.
# It is also possible to use each other Composer version channel. This includes: # It is also possible to use any other Composer version channel. This includes:
# - 2.2 (latest Composer LTS version) # - 2.2 (latest Composer LTS version)
# - stable # - stable
# - preview # - preview
# - snapshot # - snapshot
# Alternatively, an explicit Composer version may be specified, for example "2.2.18". # Alternatively, an explicit Composer version may be specified, for example "2.2.18".
# To reinstall Composer after the image was built, run "ddev debug refresh". # To reinstall Composer after the image was built, run "ddev utility rebuild".
# nodejs_version: "18" # nodejs_root: <relative_path>
# change from the default system Node.js version to another supported version, like 16, 18, 20. # Relative path to the directory containing the Node.js version file from the
# Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any # project root. Only used with "nodejs_version: auto" or "nodejs_version: engine".
# Node.js version, including v6, etc.
# You only need to configure this if you are not using nvm and you want to use a major # nodejs_version: "24"
# version that is not the default. # change from the default system Node.js version to any other version.
# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information
# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation.
# corepack_enable: false
# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm
# additional_hostnames: # additional_hostnames:
# - somename # - somename
@@ -119,7 +155,7 @@ nodejs_version: "18"
# ddev_version_constraint: "" # ddev_version_constraint: ""
# Example: # Example:
# ddev_version_constraint: ">= 1.22.4" # ddev_version_constraint: ">= 1.24.8"
# This will enforce that the running ddev version is within this constraint. # This will enforce that the running ddev version is within this constraint.
# See https://github.com/Masterminds/semver#checking-version-constraints for # See https://github.com/Masterminds/semver#checking-version-constraints for
# supported constraint formats # supported constraint formats
@@ -146,10 +182,8 @@ nodejs_version: "18"
# - "global": uses the value from the global config. # - "global": uses the value from the global config.
# - "none": disables performance optimization for this project. # - "none": disables performance optimization for this project.
# - "mutagen": enables Mutagen for this project. # - "mutagen": enables Mutagen for this project.
# - "nfs": enables NFS for this project.
# #
# See https://ddev.readthedocs.io/en/latest/users/install/performance/#nfs # See https://docs.ddev.com/en/stable/users/install/performance/#mutagen
# See https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen
# fail_on_hook_fail: False # fail_on_hook_fail: False
# Decide whether 'ddev start' should be interrupted by a failing hook # Decide whether 'ddev start' should be interrupted by a failing hook
@@ -178,10 +212,10 @@ nodejs_version: "18"
# The mailpit port is not normally bound on the host at all, instead being routed # The mailpit port is not normally bound on the host at all, instead being routed
# through ddev-router, but it can be bound directly to localhost if specified here. # through ddev-router, but it can be bound directly to localhost if specified here.
# webimage_extra_packages: [php7.4-tidy, php-bcmath] # webimage_extra_packages: ['php${DDEV_PHP_VERSION}-tidy', 'php${DDEV_PHP_VERSION}-yac']
# Extra Debian packages that are needed in the webimage can be added here # Extra Debian packages that are needed in the webimage can be added here
# dbimage_extra_packages: [telnet,netcat] # dbimage_extra_packages: [netcat, telnet, sudo]
# Extra Debian packages that are needed in the dbimage can be added here # Extra Debian packages that are needed in the dbimage can be added here
# use_dns_when_possible: true # use_dns_when_possible: true
@@ -193,22 +227,25 @@ nodejs_version: "18"
# project_tld: ddev.site # project_tld: ddev.site
# The top-level domain used for project URLs # The top-level domain used for project URLs
# The default "ddev.site" allows DNS lookup via a wildcard # The default "ddev.site" allows DNS lookup via a wildcard
# If you prefer you can change this to "ddev.local" to preserve
# pre-v1.9 behavior.
# ngrok_args: --basic-auth username:pass1234 # share_default_provider: ngrok
# Provide extra flags to the "ngrok http" command, see # The default share provider to use for "ddev share"
# https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h" # Defaults to global configuration, usually "ngrok"
# Can be "ngrok" or "cloudflared" or the name of a custom provider from .ddev/share-providers/
# share_provider_args: --basic-auth username:pass1234
# Provide extra flags to the share provider script
# See https://docs.ddev.com/en/stable/users/configuration/config/#share_provider_args
# disable_settings_management: false # disable_settings_management: false
# If true, DDEV will not create CMS-specific settings files like # If true, DDEV will not create CMS-specific settings files like
# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php # Drupal's settings.php/settings.ddev.php or TYPO3's additional.php
# In this case the user must provide all such settings. # In this case the user must provide all such settings.
# You can inject environment variables into the web container with: # You can inject environment variables into the web container with:
# web_environment: # web_environment:
# - SOMEENV=somevalue # - SOMEENV=somevalue
# - SOMEOTHERENV=someothervalue # - SOMEOTHERENV=someothervalue
# no_project_mount: false # no_project_mount: false
# (Experimental) If true, DDEV will not mount the project into the web container; # (Experimental) If true, DDEV will not mount the project into the web container;
@@ -260,7 +297,7 @@ nodejs_version: "18"
# override_config: false # override_config: false
# By default, config.*.yaml files are *merged* into the configuration # By default, config.*.yaml files are *merged* into the configuration
# But this means that some things can't be overridden # But this means that some things can't be overridden
# For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge # For example, if you have 'use_dns_when_possible: true' you can't override it with a merge
# and you can't erase existing hooks or all environment variables. # and you can't erase existing hooks or all environment variables.
# However, with "override_config: true" in a particular config.*.yaml file, # However, with "override_config: true" in a particular config.*.yaml file,
# 'use_dns_when_possible: false' can override the existing values, and # 'use_dns_when_possible: false' can override the existing values, and
@@ -270,13 +307,13 @@ nodejs_version: "18"
# web_environment: [] # web_environment: []
# or # or
# additional_hostnames: [] # additional_hostnames: []
# can have their intended affect. 'override_config' affects only behavior of the # can have their intended effect. 'override_config' affects only behavior of the
# config.*.yaml file it exists in. # config.*.yaml file it exists in.
# Many DDEV commands can be extended to run tasks before or after the # Many DDEV commands can be extended to run tasks before or after the
# DDEV command is executed, for example "post-start", "post-import-db", # DDEV command is executed, for example "post-start", "post-import-db",
# "pre-composer", "post-composer" # "pre-composer", "post-composer"
# See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more # See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more
# information on the commands that can be extended and the tasks you can define # information on the commands that can be extended and the tasks you can define
# for them. Example: # for them. Example:
#hooks: #hooks:
+2 -2
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
16 24
+2 -1
View File
@@ -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",
@@ -97,7 +98,7 @@
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"platform": { "platform": {
"php": "7.4.13" "php": "7.4.33"
}, },
"allow-plugins": { "allow-plugins": {
"typo3/cms-composer-installers": true, "typo3/cms-composer-installers": true,
Generated
+37 -35
View File
@@ -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",
@@ -1817,21 +1817,22 @@
}, },
{ {
"name": "league/oauth2-client", "name": "league/oauth2-client",
"version": "2.9.0", "version": "2.9.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/oauth2-client.git", "url": "https://github.com/thephpleague/oauth2-client.git",
"reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" "reference": "8cedfef9d01a8d1fd2ecfabb41734b17592c4b71"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/8cedfef9d01a8d1fd2ecfabb41734b17592c4b71",
"reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", "reference": "8cedfef9d01a8d1fd2ecfabb41734b17592c4b71",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", "guzzlehttp/guzzle": "^6.5.8 || ^7.8.2 || ^8.0",
"guzzlehttp/psr7": "^1.9.1 || ^2.6.3 || ^3.0",
"php": "^7.1 || >=8.0.0 <8.6.0" "php": "^7.1 || >=8.0.0 <8.6.0"
}, },
"require-dev": { "require-dev": {
@@ -1876,9 +1877,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/oauth2-client/issues", "issues": "https://github.com/thephpleague/oauth2-client/issues",
"source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0" "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.1"
}, },
"time": "2025-11-25T22:17:17+00:00" "time": "2026-09-16T13:14:31+00:00"
}, },
{ {
"name": "league/period", "name": "league/period",
@@ -2121,24 +2122,24 @@
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
"version": "2.10.1", "version": "2.11.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Masterminds/html5-php.git", "url": "https://github.com/Masterminds/html5-php.git",
"reference": "fd5018f6815fff903946d0564977b44ce8010e29" "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
"reference": "fd5018f6815fff903946d0564977b44ce8010e29", "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"php": ">=5.3.0" "php": ">=7.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -2182,9 +2183,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/Masterminds/html5-php/issues", "issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.1" "source": "https://github.com/Masterminds/html5-php/tree/2.11.0"
}, },
"time": "2026-06-23T18:43:15+00:00" "time": "2026-08-18T06:18:41+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
@@ -2641,16 +2642,16 @@
}, },
{ {
"name": "phpstan/phpdoc-parser", "name": "phpstan/phpdoc-parser",
"version": "2.3.3", "version": "2.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git", "url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2682,9 +2683,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types", "description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": { "support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues", "issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5"
}, },
"time": "2026-07-08T07:01:06+00:00" "time": "2026-08-31T16:05:28+00:00"
}, },
{ {
"name": "psr/cache", "name": "psr/cache",
@@ -5110,16 +5111,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
"version": "v1.38.1", "version": "v1.42.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git", "url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "dc21118016c039a66235cf93d96b435ffb282412" "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/51b5ff5ba85452b31ec6f55490b08148612339d9",
"reference": "dc21118016c039a66235cf93d96b435ffb282412", "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -5173,7 +5174,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.42.0"
}, },
"funding": [ "funding": [
{ {
@@ -5193,20 +5194,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-05-25T15:22:23+00:00" "time": "2026-08-24T10:51:20+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.38.0", "version": "v1.42.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -5258,7 +5259,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0"
}, },
"funding": [ "funding": [
{ {
@@ -5278,7 +5279,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-05-25T13:48:31+00:00" "time": "2026-08-07T06:33:24+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
@@ -9647,11 +9648,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
View File
@@ -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');
}); });
+1415 -1608
View File
File diff suppressed because it is too large Load Diff
+9 -23
View File
@@ -1,62 +1,48 @@
{ {
"name": "eptheme", "name": "eptheme",
"description": "EP Reisen TYPO3 website package",
"version": "3.0.0",
"author": "Björn Fromme, [email protected]",
"license": "proprietary",
"scripts": { "scripts": {
"dev": "./node_modules/.bin/encore dev --watch", "dev": "./node_modules/.bin/encore dev --watch",
"build": "NODE_ENV=production ./node_modules/.bin/encore production" "build": "NODE_ENV=production ./node_modules/.bin/encore production"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=20.19.0"
}, },
"browserslist": [
"defaults"
],
"dependencies": { "dependencies": {
"@hotwired/stimulus": "^3.2.1", "@hotwired/stimulus": "^3.2.1",
"@hotwired/stimulus-webpack-helpers": "^1.0.1", "@hotwired/stimulus-webpack-helpers": "^1.0.1",
"@iframe-resizer/parent": "^5.3.2", "@iframe-resizer/parent": "^5.3.2",
"@splidejs/splide": "^2.4.21", "@splidejs/splide": "^2.4.21",
"axios": "^1.6.2",
"cooltipz-css": "^1.6.8", "cooltipz-css": "^1.6.8",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.9", "flatpickr": "^4.6.9",
"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",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-router": "^3.5.2", "vue-session": "^1.0.0"
"vue-session": "^1.0.0",
"vuex": "^3.6.2",
"whatwg-fetch": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.16.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
"@symfony/webpack-encore": "^2.1.0", "@symfony/webpack-encore": "^4.7.0",
"@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"core-js": "^3.22.5",
"document-register-element": "^1.14.10",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"postcss-loader": "^6.2.1", "postcss-loader": "^8.2.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"sass-loader": "^12.6.0", "sass-loader": "^14.2.1",
"sass-mq": "^6.0.0", "sass-mq": "^7.0.1",
"smoothscroll-polyfill": "^0.4.4",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.1.8",
"tailwindcss-alt": "^3.0.0", "tailwindcss-alt": "^3.0.0",
"vue-custom-element": "^3.3.0",
"vue-loader": "^15.11.1", "vue-loader": "^15.11.1",
"vue-template-compiler": "^2.6.14" "vue-template-compiler": "^2.6.14"
} }
+31 -6
View File
@@ -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;
@@ -39,6 +39,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -53,6 +54,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -36,6 +36,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -50,6 +51,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -37,6 +37,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -51,6 +52,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -38,6 +38,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -52,6 +53,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -50,6 +50,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -64,6 +65,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -449,6 +451,7 @@ return [
'label' => 'Ansprechpartner', 'label' => 'Ansprechpartner',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epevents_domain_model_contact', 'foreign_table' => 'tx_epevents_domain_model_contact',
'foreign_table_where' => 'AND 1=1 order by name', 'foreign_table_where' => 'AND 1=1 order by name',
@@ -461,6 +464,7 @@ return [
'label' => 'Unterkunft', 'label' => 'Unterkunft',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epevents_domain_model_hotel', 'foreign_table' => 'tx_epevents_domain_model_hotel',
'foreign_table_where' => 'AND 1=1 order by name', 'foreign_table_where' => 'AND 1=1 order by name',
@@ -473,6 +477,7 @@ return [
'label' => 'Location', 'label' => 'Location',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epevents_domain_model_location', 'foreign_table' => 'tx_epevents_domain_model_location',
'foreign_table_where' => 'AND 1=1 order by name', 'foreign_table_where' => 'AND 1=1 order by name',
@@ -35,6 +35,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -49,6 +50,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -38,6 +38,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -52,12 +53,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epevents_domain_model_offer', 'foreign_table' => 'tx_epevents_domain_model_traveltype',
'foreign_table_where' => 'AND tx_epevents_domain_model_offer.pid=###CURRENT_PID### AND tx_epevents_domain_model_offer.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epevents_domain_model_traveltype.pid=###CURRENT_PID### AND tx_epevents_domain_model_traveltype.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -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);
@@ -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>
@@ -55,7 +55,6 @@
</template> </template>
<script> <script>
import axios from 'axios';
import $ from 'jquery'; import $ from 'jquery';
export default { export default {
@@ -82,13 +81,21 @@
load() { load() {
this.loading = true; this.loading = true;
sessionStorage.setItem('filterSettings', JSON.stringify(this.filterSettings)); sessionStorage.setItem('filterSettings', JSON.stringify(this.filterSettings));
axios({ fetch(this.endpointUri, {
url: this.endpointUri, method: 'POST',
method: 'post', credentials: 'same-origin',
data: $.param({'tx_epevents_ajax[filterSettings]': this.filterSettings}) headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
body: $.param({'tx_epevents_ajax[filterSettings]': this.filterSettings})
}).then(response => { }).then(response => {
this.offers = response.data.offers; // fetch only rejects on network errors, so surface HTTP errors here
this.filterOptions = response.data.filterOptions; // to keep the previous behaviour of falling through to .catch()
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
}).then(data => {
this.offers = data.offers;
this.filterOptions = data.filterOptions;
this.loading = false; this.loading = false;
}).catch(() => { }).catch(() => {
this.loading = false; this.loading = false;
@@ -1,7 +1,10 @@
@use "breakpoints";
@use "mixins";
.badge { .badge {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
position: relative; position: relative;
z-index: 10; z-index: 10;
@@ -27,15 +30,15 @@
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 130%; line-height: 130%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 32px; font-size: 32px;
} }
} }
.badge__outline { .badge__outline {
@include sectionFills(0.15); @include mixins.sectionFills(0.15);
} }
.badge__inner { .badge__inner {
@include sectionFills(); @include mixins.sectionFills();
} }
@@ -1,3 +1,5 @@
@use "mixins";
.banner { .banner {
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 20px;
@@ -8,7 +10,7 @@
min-height: 300px; min-height: 300px;
padding: 48px 0; padding: 48px 0;
color: white; color: white;
@include sectionBackgrounds(0.8); @include mixins.sectionBackgrounds(0.8);
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
@@ -1,14 +1,20 @@
@use "breakpoints";
@use "colors";
@use "fonts";
@use "mixins";
@use "variables";
html, html,
body { body {
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: $font-main, Sans-Serif; font-family: fonts.$font-main, Sans-Serif;
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 18px; font-size: 18px;
} }
@@ -41,13 +47,13 @@ html {
height: auto; height: auto;
margin-left: auto; margin-left: auto;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: none; display: none;
} }
} }
.navtoggle__icon { .navtoggle__icon {
fill: $color-font-main; fill: colors.$color-font-main;
transition: all 0.25s ease-in-out; transition: all 0.25s ease-in-out;
.mobilenav--open & { .mobilenav--open & {
@@ -62,7 +68,7 @@ html {
right: 2%; right: 2%;
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
opacity: 0.75; opacity: 0.75;
} }
@@ -76,12 +82,12 @@ a {
&.anchor { &.anchor {
display: block; display: block;
height: 0; height: 0;
padding-top: $header-height-mobile; padding-top: variables.$header-height-mobile;
margin-top: -$header-height-mobile; margin-top: -(variables.$header-height-mobile);
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding-top: $header-height-desktop; padding-top: variables.$header-height-desktop;
margin-top: -$header-height-desktop; margin-top: -(variables.$header-height-desktop);
} }
} }
} }
@@ -126,7 +132,7 @@ ul {
left: 0; left: 0;
content: "\25CF"; content: "\25CF";
font-size: 18px; font-size: 18px;
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
} }
} }
} }
@@ -144,8 +150,8 @@ p {
hr { hr {
max-width: 25%; max-width: 25%;
margin: 32px auto 32px 0; margin: 32px auto 32px 0;
color: $color-ruler; color: colors.$color-ruler;
background-color: $color-ruler; background-color: colors.$color-ruler;
height: 1px; height: 1px;
border: none; border: none;
} }
@@ -161,7 +167,7 @@ h3,
} }
.headline { .headline {
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
&--icon { &--icon {
display: flex; display: flex;
@@ -186,7 +192,7 @@ h1,
text-transform: uppercase; text-transform: uppercase;
padding-bottom: 18px; padding-bottom: 18px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 32px; font-size: 32px;
padding-bottom: 24px; padding-bottom: 24px;
} }
@@ -200,11 +206,11 @@ h1,
display: block; display: block;
font-size: 16px; font-size: 16px;
line-height: 120%; line-height: 120%;
color: $color-brand-primary; color: colors.$color-brand-primary;
text-transform: none; text-transform: none;
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 24px; font-size: 24px;
} }
@@ -221,7 +227,7 @@ h2,
padding-bottom: 16px; padding-bottom: 16px;
font-weight: 400; font-weight: 400;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 24px; font-size: 24px;
padding-bottom: 24px; padding-bottom: 24px;
} }
@@ -231,7 +237,7 @@ h3 {
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 18px; font-size: 18px;
padding-bottom: 8px; padding-bottom: 8px;
} }
@@ -243,7 +249,7 @@ h3 {
height: auto; height: auto;
padding-right: 16px; padding-right: 16px;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
width: 96px; width: 96px;
} }
} }
@@ -1,3 +1,6 @@
@use "breakpoints";
@use "colors";
.box { .box {
width: 100%; width: 100%;
} }
@@ -13,7 +16,7 @@
color: white; color: white;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
background-color: $color-brand-secondary; background-color: colors.$color-brand-secondary;
border: none; border: none;
width: 100%; width: 100%;
font-size: 20px; font-size: 20px;
@@ -23,18 +26,18 @@
.box__content { .box__content {
padding: 32px 16px; padding: 32px 16px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
} }
.box__form { .box__form {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
fill: $color-brand-primary; fill: colors.$color-brand-primary;
} }
&--top { &--top {
@@ -52,7 +55,7 @@
font-size: 16px; font-size: 16px;
line-height: 32px; line-height: 32px;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
font-size: 20px; font-size: 20px;
line-height: 32px; line-height: 32px;
@@ -66,7 +69,7 @@
.box__nav { .box__nav {
margin-top: -32px; margin-top: -32px;
padding-bottom: 32px; padding-bottom: 32px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
} }
.box__nav__item { .box__nav__item {
@@ -78,7 +81,7 @@
&--active, &--active,
&:hover { &:hover {
background-color: $color-brand-light; background-color: colors.$color-brand-light;
} }
} }
@@ -1,6 +1,9 @@
@use "breakpoints";
@use "colors";
.brands { .brands {
padding-top: 32px; padding-top: 32px;
border-top: 1px solid $color-divider; border-top: 1px solid colors.$color-divider;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
@@ -9,7 +12,7 @@
.brands__item { .brands__item {
width: 50%; width: 50%;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 25%; width: 25%;
} }
} }
@@ -1,7 +1,9 @@
$mq-breakpoints: ( @use "variables";
mobile: $viewport-width-mobile,
tablet: $viewport-width-tablet,
desktop: $viewport-width-desktop
);
@import "~sass-mq"; @forward "sass-mq" with (
$breakpoints: (
mobile: variables.$viewport-width-mobile,
tablet: variables.$viewport-width-tablet,
desktop: variables.$viewport-width-desktop
)
);
@@ -1,3 +1,7 @@
@use "breakpoints";
@use "colors";
@use "mixins";
.button { .button {
display: inline-block; display: inline-block;
font-weight: 700; font-weight: 700;
@@ -5,7 +9,7 @@
&--default { &--default {
display: inline-block; display: inline-block;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
padding: 4px 16px; padding: 4px 16px;
} }
@@ -13,7 +17,7 @@
&--page-header { &--page-header {
display: block; display: block;
min-width: 186px; min-width: 186px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
font-size: 18px; font-size: 18px;
padding: 0 32px 0 24px; padding: 0 32px 0 24px;
@@ -26,14 +30,14 @@
display: block; display: block;
min-width: 220px; min-width: 220px;
text-align: center; text-align: center;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
@include sectionBackgroundsFromParent(); @include mixins.sectionBackgroundsFromParent();
color: white; color: white;
line-height: 48px; line-height: 48px;
border: none; border: none;
outline: none; outline: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
box-shadow: 8px 8px 8px rgba(black, 0.2); box-shadow: 8px 8px 8px rgba(black, 0.2);
font-size: 24px; font-size: 24px;
} }
@@ -43,7 +47,7 @@
width: 100%; width: 100%;
display: inline-block; display: inline-block;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: auto; width: auto;
} }
} }
@@ -56,7 +60,7 @@
&:hover { &:hover {
background: white; background: white;
color: $color-font-main; color: colors.$color-font-main;
} }
} }
@@ -65,7 +69,7 @@
} }
&--icon { &--icon {
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -1,5 +1,8 @@
@use "breakpoints";
@use "mixins";
.card { .card {
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
&:not(.card--no-form) { &:not(.card--no-form) {
padding-bottom: 48px; padding-bottom: 48px;
} }
@@ -20,12 +23,12 @@
padding: 16px; padding: 16px;
color: white; color: white;
text-transform: uppercase; text-transform: uppercase;
@include sectionBackgrounds(0.75); @include mixins.sectionBackgrounds(0.75);
transition: background-color 0.25s ease-in-out; transition: background-color 0.25s ease-in-out;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
.card:hover & { .card:hover & {
@include sectionBackgrounds(); @include mixins.sectionBackgrounds();
} }
} }
} }
@@ -33,7 +36,7 @@
.card__form { .card__form {
display: none; display: none;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
display: block; display: block;
opacity: 0; opacity: 0;
transition: opacity 0.25s ease-in-out; transition: opacity 0.25s ease-in-out;
@@ -50,7 +53,7 @@
} }
polygon { polygon {
@include sectionFills(); @include mixins.sectionFills();
} }
} }
@@ -1,21 +1,25 @@
@use "colors";
@use "mixins";
@use "variables";
.carousel { .carousel {
&--background { &--background {
background-color: $color-background; background-color: colors.$color-background;
padding-bottom: 48px; padding-bottom: 48px;
margin-bottom: 8px; margin-bottom: 8px;
} }
@include sectionBackgrounds(0.25); @include mixins.sectionBackgrounds(0.25);
} }
.carousel__outer { .carousel__outer {
@include container($viewport-width-max); @include mixins.container(variables.$viewport-width-max);
position: relative; position: relative;
} }
.carousel__inner { .carousel__inner {
@include container($content-width-max); @include mixins.container(variables.$content-width-max);
} }
.carousel__item { .carousel__item {
@@ -36,7 +40,7 @@
height: 32px; height: 32px;
fill: white; fill: white;
@include sectionColoursFromParent; @include mixins.sectionColoursFromParent;
&--prev { &--prev {
left: -5px; left: -5px;
@@ -59,7 +63,7 @@
} }
.testimonials & { .testimonials & {
fill: $color-brand-primary; fill: colors.$color-brand-primary;
} }
} }
@@ -1,8 +1,10 @@
@use "sass:color";
//$color-brand-primary: #0084d4; //$color-brand-primary: #0084d4;
$color-brand-primary: #19a0d2; $color-brand-primary: #19a0d2;
$color-brand-secondary: #1e314b; $color-brand-secondary: #1e314b;
$color-brand-light: #8CCFE8; $color-brand-light: #8CCFE8;
$color-background: lighten(#dbdee2, 5%); $color-background: color.adjust(#dbdee2, $lightness: 5%);
$color-font-main: #1e314b; $color-font-main: #1e314b;
$color-divider: rgba($color-background, 0.8); $color-divider: rgba($color-background, 0.8);
@@ -1,3 +1,6 @@
@use "breakpoints";
@use "colors";
.configurator { .configurator {
background: url(../images/bg_configurator.jpg) no-repeat top left; background: url(../images/bg_configurator.jpg) no-repeat top left;
background-size: cover; background-size: cover;
@@ -39,7 +42,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: rgba($color-brand-secondary, 0.85); background-color: rgba(colors.$color-brand-secondary, 0.85);
color: white; color: white;
} }
@@ -51,7 +54,7 @@
display: none; display: none;
color: white; color: white;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
display: block; display: block;
} }
} }
@@ -83,11 +86,11 @@
text-decoration: none; text-decoration: none;
text-transform: uppercase; text-transform: uppercase;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
align-items: center; align-items: center;
} }
} }
@@ -98,12 +101,12 @@
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 700; font-weight: 700;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 30%; width: 30%;
margin-bottom: 0; margin-bottom: 0;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 20%; width: 20%;
margin-bottom: 0; margin-bottom: 0;
} }
@@ -113,11 +116,11 @@
width: 100%; width: 100%;
padding-right: 40px; padding-right: 40px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 70%; width: 70%;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 80%; width: 80%;
} }
} }
@@ -131,11 +134,11 @@
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 16px; font-size: 16px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
margin-bottom: 0; margin-bottom: 0;
} }
} }
@@ -151,13 +154,13 @@
text-decoration: none; text-decoration: none;
text-align: center; text-align: center;
color: white; color: white;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
border-radius: 16px; border-radius: 16px;
font-family: "fontello"; font-family: "fontello";
&.closed { &.closed {
background-color: white; background-color: white;
color: $color-brand-primary; color: colors.$color-brand-primary;
&:before { &:before {
content: '\e800'; content: '\e800';
@@ -204,7 +207,7 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
width: 50%; width: 50%;
.active & { .active & {
@@ -212,7 +215,7 @@
} }
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 25%; width: 25%;
.active & { .active & {
@@ -240,10 +243,10 @@
} }
.selected &:after { .selected &:after {
background-color: rgba($color-brand-secondary, 0.75); background-color: rgba(colors.$color-brand-secondary, 0.75);
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
&:hover:after { &:hover:after {
content: ""; content: "";
width: 100%; width: 100%;
@@ -251,7 +254,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
background-color: rgba($color-brand-primary, 0.5); background-color: rgba(colors.$color-brand-primary, 0.5);
} }
} }
@@ -260,12 +263,12 @@
background-position: center 32px; background-position: center 32px;
background-size: 96px; background-size: 96px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
background-position: center 16px; background-position: center 16px;
background-size: 128px; background-size: 128px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
background-position: center 8px; background-position: center 8px;
background-size: 96px; background-size: 96px;
} }
@@ -382,7 +385,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
@@ -391,7 +394,7 @@
width: 100%; width: 100%;
padding-bottom: 16px; padding-bottom: 16px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 66.6666%; width: 66.6666%;
padding-right: 32px; padding-right: 32px;
} }
@@ -401,7 +404,7 @@
width: 100%; width: 100%;
padding-bottom: 16px; padding-bottom: 16px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -456,7 +459,7 @@
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;
background-color: white; background-color: white;
color: $color-brand-primary; color: colors.$color-brand-primary;
border-radius: 16px; border-radius: 16px;
font-family: 'fontello'; font-family: 'fontello';
@@ -1,3 +1,6 @@
// .headline* live in _base.scss; @extend needs that module loaded explicitly.
@use "base";
.cookiefirst { .cookiefirst {
h2 { h2 {
@extend .headline; @extend .headline;
@@ -1,10 +1,14 @@
@use "breakpoints";
@use "colors";
@use "mixins";
.form { .form {
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative; position: relative;
max-width: 100%; max-width: 100%;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
&:not(&--horizontal) { &:not(&--horizontal) {
max-width: 75%; max-width: 75%;
} }
@@ -16,7 +20,7 @@
&--horizontal { &--horizontal {
width: 100%; width: 100%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -42,7 +46,7 @@
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: $color-brand-light; background-color: colors.$color-brand-light;
color: white; color: white;
border-radius: 8px; border-radius: 8px;
} }
@@ -50,7 +54,7 @@
&--mobile { &--mobile {
display: block; display: block;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: none; display: none;
} }
} }
@@ -58,7 +62,7 @@
&--tablet { &--tablet {
display: none; display: none;
@include mq($from: tablet, $until: desktop) { @include breakpoints.mq($from: tablet, $until: desktop) {
display: block; display: block;
} }
} }
@@ -66,7 +70,7 @@
&--desktop { &--desktop {
display: none; display: none;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
display: block; display: block;
} }
} }
@@ -79,7 +83,7 @@
padding-top: 20px; padding-top: 20px;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -88,20 +92,20 @@
.form__item__error { .form__item__error {
padding: 8px; padding: 8px;
font-size: 90%; font-size: 90%;
color: $color-formerror; color: colors.$color-formerror;
} }
.form__field { .form__field {
@include nooutline; @include mixins.nooutline;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
width: 100%; width: 100%;
padding: 8px 16px; padding: 8px 16px;
color: white; color: white;
background-color: $color-brand-light; background-color: colors.$color-brand-light;
.form--box & { .form--box & {
background-color: $color-brand-light; background-color: colors.$color-brand-light;
color: white; color: white;
} }
@@ -114,10 +118,10 @@
display: block; display: block;
padding: 8px; padding: 8px;
width: 100%; width: 100%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 50%; width: 50%;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 25%; width: 25%;
} }
} }
@@ -127,7 +131,7 @@
} }
.has-errors & { .has-errors & {
border: 1px solid $color-formerror; border: 1px solid colors.$color-formerror;
} }
} }
@@ -158,7 +162,7 @@
display: block; display: block;
width: 100%; width: 100%;
color: white; color: white;
background-color: $color-brand-secondary; background-color: colors.$color-brand-secondary;
border: none; border: none;
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
@@ -166,7 +170,7 @@
padding: 8px 32px; padding: 8px 32px;
margin-bottom: 16px; margin-bottom: 16px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: inline-block; display: inline-block;
width: auto; width: auto;
margin-bottom: 0; margin-bottom: 0;
@@ -189,10 +193,10 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: rgba($color-divider, 0.75); background-color: rgba(colors.$color-divider, 0.75);
.form--box & { .form--box & {
background-color: rgba($color-brand-primary, 0.75); background-color: rgba(colors.$color-brand-primary, 0.75);
} }
} }
@@ -1,7 +1,10 @@
@use "breakpoints";
@use "colors";
.helpernav { .helpernav {
display: none; display: none;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
display: block; display: block;
position: relative; position: relative;
} }
@@ -24,7 +27,7 @@
z-index: 10; z-index: 10;
min-width: 300px; min-width: 300px;
padding: 16px 32px; padding: 16px 32px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
} }
.helpernav__item { .helpernav__item {
@@ -32,7 +35,7 @@
&:hover:not(.helpernav__item--disabled) { &:hover:not(.helpernav__item--disabled) {
& > .helpernav__link { & > .helpernav__link {
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
} }
& > .helpernav__subnav { & > .helpernav__subnav {
@@ -1,11 +1,16 @@
@use "breakpoints";
@use "colors";
@use "mixins";
@use "variables";
.herounit { .herounit {
position: relative; position: relative;
width: 100%; width: 100%;
max-width: $viewport-width-max; max-width: variables.$viewport-width-max;
margin: 0 auto; margin: 0 auto;
&--indent { &--indent {
max-width: $content-width-max - 16px; max-width: variables.$content-width-max - 16px;
} }
} }
@@ -16,22 +21,22 @@
height: 240px; height: 240px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
height: 384px; height: 384px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
height: 420px; height: 420px;
} }
.herounit--large & { .herounit--large & {
height: 300px; height: 300px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
height: 480px; height: 480px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
height: 700px; height: 700px;
} }
} }
@@ -39,7 +44,7 @@
.herounit--indent & { .herounit--indent & {
height: 320px; height: 320px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
height: 480px; height: 480px;
} }
} }
@@ -48,7 +53,7 @@
.herounit__mask { .herounit__mask {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
position: absolute; position: absolute;
z-index: 10; z-index: 10;
top: 0; top: 0;
@@ -67,9 +72,9 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 32px; padding: 32px;
background-color: rgba($color-brand-secondary, 0.2); background-color: rgba(colors.$color-brand-secondary, 0.2);
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
background-color: transparent; background-color: transparent;
padding: 64px 16px 0 96px; padding: 64px 16px 0 96px;
max-width: 45%; max-width: 45%;
@@ -83,7 +88,7 @@
font-size: 32px; font-size: 32px;
line-height: 95%; line-height: 95%;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
font-size: 48px; font-size: 48px;
} }
@@ -101,10 +106,10 @@
span { span {
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
color: $color-brand-primary; color: colors.$color-brand-primary;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
font-size: 56px; font-size: 56px;
} }
} }
@@ -120,7 +125,7 @@
color: white; color: white;
text-transform: none; text-transform: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 20px; font-size: 20px;
} }
@@ -128,12 +133,12 @@
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 24px; font-size: 24px;
padding-bottom: 48px; padding-bottom: 48px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
font-size: 32px; font-size: 32px;
padding-bottom: 96px; padding-bottom: 96px;
} }
@@ -146,7 +151,7 @@
line-height: 120%; line-height: 120%;
color: white; color: white;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
} }
} }
@@ -154,7 +159,7 @@
.herounit__icon { .herounit__icon {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
@@ -173,11 +178,11 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
top: 85%; top: 85%;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
@@ -190,34 +195,34 @@
height: 48px; height: 48px;
border: none; border: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
} }
& > polygon { & > polygon {
fill: $color-brand-primary; fill: colors.$color-brand-primary;
@include sectionFills(); @include mixins.sectionFills();
} }
} }
#gradient__from { #gradient__from {
stop-color: $color-brand-secondary; stop-color: colors.$color-brand-secondary;
} }
#gradient__to { #gradient__to {
.section-default & { .section-default & {
stop-color: $color-brand-primary; stop-color: colors.$color-brand-primary;
} }
.section-incentives & { .section-incentives & {
stop-color: $color-incentives; stop-color: colors.$color-incentives;
} }
.section-teambuilding & { .section-teambuilding & {
stop-color: $color-teambuilding; stop-color: colors.$color-teambuilding;
} }
.secolor-meeting & { .secolor-meeting & {
stop-color: $color-meetings; stop-color: colors.$color-meetings;
} }
.secolor-events & { .secolor-events & {
stop-color: $color-events; stop-color: colors.$color-events;
} }
} }
@@ -1,9 +1,12 @@
@use "breakpoints";
@use "colors";
.iconbar { .iconbar {
@apply hidden lg:block fixed z-100 top-[30vh] right-0 overflow-hidden; @apply hidden lg:block fixed z-100 top-[30vh] right-0 overflow-hidden;
} }
.iconbar { .iconbar {
display: none; display: none;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
display: block; display: block;
} }
position: fixed; position: fixed;
@@ -20,7 +23,7 @@
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
background-color: $color-brand-light; background-color: colors.$color-brand-light;
margin-bottom: 2px; margin-bottom: 2px;
&:hover { &:hover {
@@ -30,7 +33,7 @@
.iconbar__icon { .iconbar__icon {
padding: 4px; padding: 4px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
svg { svg {
display: block; display: block;
@@ -1,3 +1,6 @@
@use "breakpoints";
@use "mixins";
.iconnav { .iconnav {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -21,7 +24,7 @@
opacity: 1; opacity: 1;
} }
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
width: 25%; width: 25%;
} }
} }
@@ -36,5 +39,5 @@
.iconnav__label { .iconnav__label {
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
@include sectionColours(); @include mixins.sectionColours();
} }
@@ -1,7 +1,10 @@
@use "breakpoints";
@use "mixins";
main { main {
padding-top: 96px; padding-top: 96px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding-top: 140px; padding-top: 140px;
} }
} }
@@ -10,7 +13,7 @@ main {
width: 100%; width: 100%;
padding-top: 32px; padding-top: 32px;
padding-bottom: 32px; padding-bottom: 32px;
@include clearfix(); @include mixins.clearfix();
&.nopad { &.nopad {
&--bottom { &--bottom {
@@ -23,18 +26,18 @@ main {
} }
.content__title { .content__title {
@include container(); @include mixins.container();
text-align: center; text-align: center;
} }
.content__inner { .content__inner {
@include container(); @include mixins.container();
.content--has-sidebar & { .content--has-sidebar & {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
@@ -45,40 +48,40 @@ main {
padding: 0 0 32px; padding: 0 0 32px;
overflow: hidden; overflow: hidden;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding: 0; padding: 0;
} }
} }
.content__plain { .content__plain {
@include clearfix(); @include mixins.clearfix();
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding-right: 96px; padding-right: 96px;
} }
} }
.content__background { .content__background {
@include clearfix(); @include mixins.clearfix();
@include sectionBackgrounds(0.15); @include mixins.sectionBackgrounds(0.15);
padding: 24px 48px 24px 32px; padding: 24px 48px 24px 32px;
} }
.content__float { .content__float {
@include clearfix(); @include mixins.clearfix();
} }
.content__sidebar { .content__sidebar {
width: 100%; width: 100%;
padding: 32px 0; padding: 32px 0;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
flex: 0 0 320px; flex: 0 0 320px;
padding: 0 0 0 32px; padding: 0 0 0 32px;
} }
} }
.content__form { .content__form {
@include sectionFills(0.15); @include mixins.sectionFills(0.15);
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
@@ -93,7 +96,7 @@ main {
.desktop-only { .desktop-only {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
} }
} }
@@ -1,4 +1,6 @@
@use "sass:math"; @use "sass:math";
@use "variables" as *;
@use "colors" as *;
@mixin clearfix { @mixin clearfix {
&::after { &::after {
@@ -1,3 +1,6 @@
@use "breakpoints";
@use "colors";
.mobilenav { .mobilenav {
position: fixed; position: fixed;
z-index: 50; z-index: 50;
@@ -7,10 +10,10 @@
height: 0; height: 0;
padding-top: 96px; padding-top: 96px;
overflow: hidden; overflow: hidden;
background: rgba($color-brand-secondary, 0.9); background: rgba(colors.$color-brand-secondary, 0.9);
transition: height 0.25s ease-in-out; transition: height 0.25s ease-in-out;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
display: none; display: none;
} }
@@ -1,3 +1,5 @@
@use "breakpoints";
.news-entry { .news-entry {
width: 100%; width: 100%;
&:not(:last-child) { &:not(:last-child) {
@@ -13,7 +15,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
flex-direction: row; flex-direction: row;
} }
} }
@@ -22,7 +24,7 @@
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 50%; width: 50%;
} }
} }
@@ -39,7 +41,7 @@
.news-entry__teaser { .news-entry__teaser {
width: 100%; width: 100%;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
padding-left: 24px; padding-left: 24px;
width: 50%; width: 50%;
} }
@@ -1,3 +1,5 @@
@use "breakpoints";
.offerlist { .offerlist {
position: relative; position: relative;
} }
@@ -13,7 +15,7 @@
} }
.offerlist__teasers { .offerlist__teasers {
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -24,13 +26,13 @@
text-decoration: none; text-decoration: none;
margin-bottom: 32px; margin-bottom: 32px;
display: block; display: block;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
display: flex; display: flex;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 50%; width: 50%;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -1,25 +1,30 @@
@use "breakpoints";
@use "colors";
@use "mixins";
@use "variables";
.page-footer { .page-footer {
width: 100%; width: 100%;
background: linear-gradient(to right, $color-brand-secondary, $color-brand-primary); background: linear-gradient(to right, colors.$color-brand-secondary, colors.$color-brand-primary);
color: white; color: white;
padding-top: 16px; padding-top: 16px;
padding-bottom: 16px; padding-bottom: 16px;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
padding-top: 48px; padding-top: 48px;
padding-bottom: 48px; padding-bottom: 48px;
} }
} }
.page-footer__inner { .page-footer__inner {
@include container($content-width-max); @include mixins.container(variables.$content-width-max);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
border-bottom: 1px solid white; border-bottom: 1px solid white;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding-bottom: 48px; padding-bottom: 48px;
} }
} }
@@ -33,14 +38,14 @@
text-align: left; text-align: left;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 20%; width: 20%;
} }
&--hide-mobile { &--hide-mobile {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
} }
} }
@@ -60,14 +65,14 @@
} }
.page-footer__bottom { .page-footer__bottom {
@include container($content-width-max); @include mixins.container(variables.$content-width-max);
padding-top: 32px; padding-top: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
} }
@@ -76,7 +81,7 @@
.page-footer__bottom__column { .page-footer__bottom__column {
width: 100%; width: 100%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -86,7 +91,7 @@
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
justify-content: flex-start; justify-content: flex-start;
} }
} }
@@ -106,7 +111,7 @@
font-weight: 700; font-weight: 700;
text-align: center; text-align: center;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
text-align: left; text-align: left;
} }
} }
@@ -115,7 +120,7 @@
text-align: center; text-align: center;
padding-top: 32px; padding-top: 32px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
text-align: right; text-align: right;
padding-top: 0; padding-top: 0;
} }
@@ -138,7 +143,7 @@
text-align: center; text-align: center;
padding-top: 32px; padding-top: 32px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
text-align: left; text-align: left;
padding-top: 0; padding-top: 0;
} }
@@ -1,3 +1,7 @@
@use "breakpoints";
@use "colors";
@use "mixins";
.page-header { .page-header {
width: 100%; width: 100%;
position: fixed; position: fixed;
@@ -5,28 +9,28 @@
left: 0; left: 0;
z-index: 1000; z-index: 1000;
background: white; background: white;
box-shadow: 0 2px 2px rgba($color-background, 0.5); box-shadow: 0 2px 2px rgba(colors.$color-background, 0.5);
} }
.page-header__bar { .page-header__bar {
background: linear-gradient(to right, $color-brand-primary 50%, $color-brand-secondary); background: linear-gradient(to right, colors.$color-brand-primary 50%, colors.$color-brand-secondary);
} }
.page-header__bar__main { .page-header__bar__main {
@include container(); @include mixins.container();
color: white; color: white;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
justify-content: space-between; justify-content: space-between;
} }
} }
.page-header__bar__search { .page-header__bar__search {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
width: 50%; width: 50%;
} }
@@ -37,7 +41,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 50%; width: 50%;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -53,14 +57,14 @@
.page-header__bar__divider { .page-header__bar__divider {
display: none; display: none;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
display: inline-block; display: inline-block;
padding: 12px 8px; padding: 12px 8px;
} }
} }
.page-header__main { .page-header__main {
@include container(); @include mixins.container();
padding: 16px; padding: 16px;
display: flex; display: flex;
display: -ms-flexbox; display: -ms-flexbox;
@@ -68,11 +72,11 @@
align-items: center; align-items: center;
-ms-flex-align: center; -ms-flex-align: center;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding: 32px 16px 24px; padding: 32px 16px 24px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
-ms-flex-align: end; -ms-flex-align: end;
@@ -98,7 +102,7 @@
.page-header__sitenav { .page-header__sitenav {
display: none; display: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
display: block; display: block;
} }
} }
@@ -106,7 +110,7 @@
.page-header__button { .page-header__button {
display: none; display: none;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
position: relative; position: relative;
display: block; display: block;
min-width: 220px; min-width: 220px;
@@ -128,6 +132,6 @@
border: none; border: none;
& > polygon { & > polygon {
fill: $color-brand-primary; fill: colors.$color-brand-primary;
} }
} }
@@ -1,3 +1,5 @@
@use "colors";
.paginator { .paginator {
list-style: none; list-style: none;
margin: 0; margin: 0;
@@ -10,7 +12,7 @@
.paginator__item { .paginator__item {
display: inline-block; display: inline-block;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
padding: 4px; padding: 4px;
text-decoration: none; text-decoration: none;
@@ -19,8 +21,8 @@
} }
&--current { &--current {
border: 1px solid $color-brand-primary; border: 1px solid colors.$color-brand-primary;
background-color: white; background-color: white;
color: $color-brand-primary; color: colors.$color-brand-primary;
} }
} }
@@ -1,24 +1,27 @@
@use "breakpoints";
@use "mixins";
.searchresult {} .searchresult {}
.searchresult__info { .searchresult__info {
@include container(); @include mixins.container();
} }
.searchresult-item { .searchresult-item {
text-decoration: none; text-decoration: none;
margin-bottom: 32px; margin-bottom: 32px;
display: block; display: block;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
display: flex; display: flex;
} }
} }
.searchresult-item__card { .searchresult-item__card {
width: 100%; width: 100%;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
width: 240px; width: 240px;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -27,8 +30,8 @@
margin: 0 8px; margin: 0 8px;
padding: 32px; padding: 32px;
color: white; color: white;
@include sectionBackgrounds(); @include mixins.sectionBackgrounds();
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
margin: 0; margin: 0;
flex: 1; flex: 1;
} }
@@ -1,5 +1,8 @@
@use "breakpoints";
@use "mixins";
.sitenav { .sitenav {
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
padding-right: 64px; padding-right: 64px;
} }
} }
@@ -25,7 +28,7 @@
&--active, &--active,
&:hover { &:hover {
border-bottom: 3px solid; border-bottom: 3px solid;
@include sectionColours(); @include mixins.sectionColours();
} }
} }
@@ -1,3 +1,5 @@
@use "breakpoints";
.slider {} .slider {}
.slider__arrow { .slider__arrow {
@@ -38,7 +40,7 @@
top: 30%; top: 30%;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
@@ -1,11 +1,14 @@
@use "breakpoints";
@use "mixins";
.statement { .statement {
@include container(); @include mixins.container();
padding-top: 32px; padding-top: 32px;
padding-bottom: 48px; padding-bottom: 48px;
} }
.statement__inner { .statement__inner {
@include sectionBackgrounds(); @include mixins.sectionBackgrounds();
box-shadow: 0 5px 10px rgba(black, 0.2); box-shadow: 0 5px 10px rgba(black, 0.2);
text-align: center; text-align: center;
padding-top: 48px; padding-top: 48px;
@@ -18,7 +21,7 @@
font-size: 24px; font-size: 24px;
line-height: 105%; line-height: 105%;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 48px; font-size: 48px;
} }
} }
@@ -1,3 +1,6 @@
@use "breakpoints";
@use "colors";
.team { .team {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -12,11 +15,11 @@
max-width: 400px; max-width: 400px;
padding: 0 16px 64px; padding: 0 16px 64px;
@include mq($until: desktop) { @include breakpoints.mq($until: desktop) {
margin: 0 auto; margin: 0 auto;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 50%; width: 50%;
max-width: 100%; max-width: 100%;
} }
@@ -57,10 +60,10 @@
.team__position { .team__position {
display: block; display: block;
color: $color-brand-primary; color: colors.$color-brand-primary;
} }
.team__contact__link { .team__contact__link {
text-decoration: none; text-decoration: none;
color: $color-brand-primary; color: colors.$color-brand-primary;
} }
@@ -1,12 +1,15 @@
@use "breakpoints";
@use "mixins";
.teasers { .teasers {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@include sectionBackgrounds(0.15); @include mixins.sectionBackgrounds(0.15);
margin: 0 -16px 0; margin: 0 -16px 0;
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
flex-direction: row; flex-direction: row;
} }
} }
@@ -19,15 +22,15 @@
cursor: pointer; cursor: pointer;
} }
@include mq($until: tablet) { @include breakpoints.mq($until: tablet) {
padding-bottom: 16px; padding-bottom: 16px;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 50%; width: 50%;
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 33.3333%; width: 33.3333%;
} }
} }
@@ -40,12 +43,12 @@
} }
.teaser__caption { .teaser__caption {
@include sectionBackgroundsFromParent(); @include mixins.sectionBackgroundsFromParent();
padding: 16px 16px 0; padding: 16px 16px 0;
} }
.teaser__form { .teaser__form {
@include sectionFills(); @include mixins.sectionFills();
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
@@ -1,17 +1,20 @@
@use "breakpoints";
@use "colors";
.testimonial { .testimonial {
width: 100%; width: 100%;
padding: 0 32px; padding: 0 32px;
text-align: center; text-align: center;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 80%; font-size: 80%;
width: 50%; width: 50%;
&:not(:last-child) { &:not(:last-child) {
border-right: 1px solid $color-divider; border-right: 1px solid colors.$color-divider;
} }
} }
@include mq($from: desktop) { @include breakpoints.mq($from: desktop) {
width: 25%; width: 25%;
} }
} }
@@ -1,3 +1,7 @@
@use "breakpoints";
@use "colors";
@use "mixins";
.tiles { .tiles {
padding-bottom: 0; padding-bottom: 0;
} }
@@ -15,7 +19,7 @@
padding: 16px; padding: 16px;
text-decoration: none; text-decoration: none;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 50%; width: 50%;
padding: 16px; padding: 16px;
} }
@@ -26,12 +30,12 @@
background-size: cover; background-size: cover;
background-position: center; background-position: center;
@include mq($from: mobile) { @include breakpoints.mq($from: mobile) {
min-height: 250px; min-height: 250px;
padding: 32px; padding: 32px;
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding: 32px 32px 0; padding: 32px 32px 0;
min-height: 375px; min-height: 375px;
} }
@@ -42,33 +46,33 @@
color: white; color: white;
text-align: center; text-align: center;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
width: 70%; width: 70%;
margin: 0 auto; margin: 0 auto;
} }
} }
.tile__caption__inner { .tile__caption__inner {
@include sectionBackgrounds(0.75); @include mixins.sectionBackgrounds(0.75);
width: 100%; width: 100%;
padding: 24px 0; padding: 24px 0;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
padding: 32px 0; padding: 32px 0;
.tile:hover .section-incentives & { .tile:hover .section-incentives & {
background-color: $color-incentives; background-color: colors.$color-incentives;
} }
.tile:hover .section-teambuilding & { .tile:hover .section-teambuilding & {
background-color: $color-teambuilding; background-color: colors.$color-teambuilding;
} }
.tile:hover .section-meetings & { .tile:hover .section-meetings & {
background-color: $color-meetings; background-color: colors.$color-meetings;
} }
.tile:hover .section-events & { .tile:hover .section-events & {
background-color: $color-events; background-color: colors.$color-events;
} }
} }
} }
@@ -77,7 +81,7 @@
display: none; display: none;
padding: 16px 32px 0; padding: 16px 32px 0;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
.tile:hover & { .tile:hover & {
display: block; display: block;
} }
@@ -91,10 +95,10 @@
margin-top: -1px; margin-top: -1px;
polygon { polygon {
@include sectionFills(); @include mixins.sectionFills();
} }
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
.tile:hover & { .tile:hover & {
display: block; display: block;
} }
@@ -114,7 +118,7 @@
font-weight: 700; font-weight: 700;
min-height: 54px; min-height: 54px;
@include mq($from: tablet) { @include breakpoints.mq($from: tablet) {
font-size: 24px; font-size: 24px;
} }
} }
@@ -1,5 +1,7 @@
@use "mixins";
.video { .video {
@include container(); @include mixins.container();
padding-top: 32px; padding-top: 32px;
padding-bottom: 32px; padding-bottom: 32px;
} }
@@ -1,39 +1,39 @@
@import "variables"; @use "variables";
@import "mixins"; @use "mixins";
@import "breakpoints"; @use "breakpoints";
@import "colors"; @use "colors";
@import "fonts"; @use "fonts";
@import "base"; @use "base";
@import "buttons"; @use "buttons";
@import "page-header"; @use "page-header";
@import "page-footer"; @use "page-footer";
@import "layout"; @use "layout";
@import "sitenav"; @use "sitenav";
@import "helpernav"; @use "helpernav";
@import "mobilenav"; @use "mobilenav";
@import "herounit"; @use "herounit";
@import "carousel"; @use "carousel";
@import "slider"; @use "slider";
@import "configurator"; @use "configurator";
@import "statement"; @use "statement";
@import "testimonial"; @use "testimonial";
@import "brands"; @use "brands";
@import "iconbar"; @use "iconbar";
@import "tiles"; @use "tiles";
@import "card"; @use "card";
@import "box"; @use "box";
@import "banner"; @use "banner";
@import "iconnav"; @use "iconnav";
@import "forms"; @use "forms";
@import "teasers"; @use "teasers";
@import "badge"; @use "badge";
@import "team"; @use "team";
@import "search"; @use "search";
@import "offerlist"; @use "offerlist";
@import "paginator"; @use "paginator";
@import "news"; @use "news";
@import "datepicker"; @use "datepicker";
@import "cookiefirst"; @use "cookiefirst";
@import "video"; @use "video";
@@ -1,18 +1,14 @@
@import "mixins"; @use "mixins";
@import "colors"; @use "colors";
@font-face { @font-face {
font-family: 'Cairo'; font-family: 'Cairo';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('../fonts/cairo-v2-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
src: local('Cairo'), local('Cairo-Regular'), src: local('Cairo'), local('Cairo-Regular'),
url('../fonts/cairo-v2-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('../fonts/cairo-v2-latin-ext_latin-regular.woff2') format('woff2'),
url('../fonts/cairo-v2-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ url('../fonts/cairo-v2-latin-ext_latin-regular.woff') format('woff');
url('../fonts/cairo-v2-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
url('../fonts/cairo-v2-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/cairo-v2-latin-ext_latin-regular.svg#Cairo') format('svg'); /* Legacy iOS */
} }
@font-face { @font-face {
@@ -20,13 +16,9 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('../fonts/cairo-v2-latin-ext_latin-700.eot'); /* IE9 Compat Modes */
src: local('Cairo Bold'), local('Cairo-Bold'), src: local('Cairo Bold'), local('Cairo-Bold'),
url('../fonts/cairo-v2-latin-ext_latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('../fonts/cairo-v2-latin-ext_latin-700.woff2') format('woff2'),
url('../fonts/cairo-v2-latin-ext_latin-700.woff2') format('woff2'), /* Super Modern Browsers */ url('../fonts/cairo-v2-latin-ext_latin-700.woff') format('woff');
url('../fonts/cairo-v2-latin-ext_latin-700.woff') format('woff'), /* Modern Browsers */
url('../fonts/cairo-v2-latin-ext_latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
url('../fonts/cairo-v2-latin-ext_latin-700.svg#Cairo') format('svg'); /* Legacy iOS */
} }
$font-main: 'Cairo'; $font-main: 'Cairo';
@@ -34,12 +26,8 @@ $font-main: 'Cairo';
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
font-display: swap; font-display: swap;
src: url('../fonts/fontello.eot?39986861'); src: url('../fonts/fontello.woff2?39986861') format('woff2'),
src: url('../fonts/fontello.eot?39986861#iefix') format('embedded-opentype'), url('../fonts/fontello.woff?39986861') format('woff');
url('../fonts/fontello.woff2?39986861') format('woff2'),
url('../fonts/fontello.woff?39986861') format('woff'),
url('../fonts/fontello.ttf?39986861') format('truetype'),
url('../fonts/fontello.svg?39986861#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@@ -123,7 +111,7 @@ ul {
left: 0; left: 0;
content: "\25CF"; content: "\25CF";
font-size: 18px; font-size: 18px;
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
} }
} }
} }
@@ -141,8 +129,8 @@ p {
hr { hr {
max-width: 25%; max-width: 25%;
margin: 32px auto 32px 0; margin: 32px auto 32px 0;
color: $color-ruler; color: colors.$color-ruler;
background-color: $color-ruler; background-color: colors.$color-ruler;
height: 1px; height: 1px;
border: none; border: none;
} }
@@ -158,7 +146,7 @@ h3,
} }
.headline { .headline {
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
&--icon { &--icon {
display: flex; display: flex;
@@ -192,9 +180,9 @@ h1,
display: block; display: block;
font-size: 16px; font-size: 16px;
line-height: 120%; line-height: 120%;
color: $color-brand-primary; color: colors.$color-brand-primary;
text-transform: none; text-transform: none;
@include sectionColoursFromParent(); @include mixins.sectionColoursFromParent();
&--box { &--box {
color: white; color: white;
@@ -229,7 +217,7 @@ h3 {
&--default { &--default {
display: inline-block; display: inline-block;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
padding: 4px 16px; padding: 4px 16px;
} }
@@ -237,7 +225,7 @@ h3 {
&--page-header { &--page-header {
display: block; display: block;
min-width: 186px; min-width: 186px;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
font-size: 18px; font-size: 18px;
padding: 0 32px 0 24px; padding: 0 32px 0 24px;
@@ -250,8 +238,8 @@ h3 {
display: block; display: block;
min-width: 220px; min-width: 220px;
text-align: center; text-align: center;
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
@include sectionBackgroundsFromParent(); @include mixins.sectionBackgroundsFromParent();
color: white; color: white;
line-height: 48px; line-height: 48px;
border: none; border: none;
@@ -271,7 +259,7 @@ h3 {
&:hover { &:hover {
background: white; background: white;
color: $color-font-main; color: colors.$color-font-main;
} }
} }
@@ -280,7 +268,7 @@ h3 {
} }
&--icon { &--icon {
background-color: $color-brand-primary; background-color: colors.$color-brand-primary;
color: white; color: white;
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -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
{}
@@ -49,8 +49,22 @@ class ApiClient implements LoggerAwareInterface
*/ */
private const ACCESS_TOKEN_DEFAULT_TTL = 300; private const ACCESS_TOKEN_DEFAULT_TTL = 300;
/**
* Status codes that mean the token was rejected rather than the request being wrong.
*/
private const UNAUTHORIZED_STATUS_CODES = [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN];
private static ?AccessToken $accessToken = null; private static ?AccessToken $accessToken = null;
/**
* Shared on purpose. Symfony's CurlHttpClient keeps its connection pool and TLS session
* cache in a CurlClientState owned by the instance, so building a client per call throws
* both away and forces a fresh DNS lookup, TCP connect and TLS handshake every time -
* TravelinfoController alone makes two calls per page render. The bearer token is passed
* per request rather than as a client default so one instance can outlive one token.
*/
private static ?HttpClientInterface $httpClient = null;
private ?FrontendInterface $cache = null; private ?FrontendInterface $cache = null;
private ?array $configuration = null; private ?array $configuration = null;
@@ -100,7 +114,25 @@ 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');
$this->drainResponse($response);
}
/**
* @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');
$this->drainResponse($response);
} }
/** /**
@@ -183,6 +215,30 @@ class ApiClient implements LoggerAwareInterface
*/ */
private function request(string $method, string $uri, array $options = []): ResponseInterface private function request(string $method, string $uri, array $options = []): ResponseInterface
{ {
$response = $this->sendRequest($method, $uri, $options);
if (false === in_array($this->readStatusCode($response, $method, $uri), self::UNAUTHORIZED_STATUS_CODES, true)) {
return $response;
}
// The token outlives a single PHP request, so one the API has stopped accepting - after
// a MyEP restart or a revoked client - would keep failing every call for the rest of its
// cached lifetime unless it is actively discarded here.
$this->logWarning(sprintf('MyEP API rejected the cached access token for %s %s, re-authenticating once', $method, $uri));
$this->discardAccessToken();
return $this->sendRequest($method, $uri, $options);
}
/**
* @throws ApiException
*/
private function sendRequest(string $method, string $uri, array $options): ResponseInterface
{
// Resolved here rather than baked into the client's default options so that the shared
// client survives a token change, and so the retry above picks up the fresh token.
$options['auth_bearer'] = $this->getAccessToken($this->getConfiguration())->getToken();
try { try {
return $this->getHttpClient()->request($method, $uri, $options); return $this->getHttpClient()->request($method, $uri, $options);
} catch (TransportExceptionInterface $e) { } catch (TransportExceptionInterface $e) {
@@ -199,13 +255,7 @@ class ApiClient implements LoggerAwareInterface
*/ */
private function assertStatusCode(ResponseInterface $response, array $expectedStatusCodes, string $method, string $uri): void private function assertStatusCode(ResponseInterface $response, array $expectedStatusCodes, string $method, string $uri): void
{ {
try { $statusCode = $this->readStatusCode($response, $method, $uri);
$statusCode = $response->getStatusCode();
} catch (TransportExceptionInterface $e) {
$message = sprintf('MyEP API transport error while reading status for %s %s: %s', $method, $uri, $e->getMessage());
$this->logError($message, ['exception' => $e]);
throw new ApiException($message, 0, $e);
}
if (true === in_array($statusCode, $expectedStatusCodes, true)) { if (true === in_array($statusCode, $expectedStatusCodes, true)) {
return; return;
@@ -217,6 +267,39 @@ class ApiClient implements LoggerAwareInterface
throw new ApiException($message, $statusCode); throw new ApiException($message, $statusCode);
} }
/**
* Reading the status completes the transfer, so this is also what turns the lazy response
* returned by the client into a finished request. Symfony keeps the status on the response,
* so calling this twice costs nothing.
*
* @throws ApiException
*/
private function readStatusCode(ResponseInterface $response, string $method, string $uri): int
{
try {
return $response->getStatusCode();
} catch (TransportExceptionInterface $e) {
$message = sprintf('MyEP API transport error while reading status for %s %s: %s', $method, $uri, $e->getMessage());
$this->logError($message, ['exception' => $e]);
throw new ApiException($message, 0, $e);
}
}
/**
* Reads and discards a body nobody else is going to look at. Destroying an unconsumed
* response cancels the transfer, which closes the connection instead of returning it to
* the pool, so the next call would have to handshake again.
*/
private function drainResponse(ResponseInterface $response): void
{
try {
$response->getContent(false);
} catch (TransportExceptionInterface $e) {
// The status has already been asserted, so a failure here costs nothing but the
// connection reuse this method exists for.
}
}
/** /**
* @throws ApiException * @throws ApiException
*/ */
@@ -237,13 +320,13 @@ class ApiClient implements LoggerAwareInterface
private function getHttpClient(): HttpClientInterface private function getHttpClient(): HttpClientInterface
{ {
if (null !== static::$httpClient) {
return static::$httpClient;
}
$config = $this->getConfiguration(); $config = $this->getConfiguration();
static::$accessToken = $this->getAccessToken($config); $options = [];
$options = [
'auth_bearer' => static::$accessToken->getToken(),
];
// Without these the client falls back to PHP's default_socket_timeout (commonly 60s), // Without these the client falls back to PHP's default_socket_timeout (commonly 60s),
// so a stalled API pins a PHP worker for a minute per request. The defaults live in // so a stalled API pins a PHP worker for a minute per request. The defaults live in
@@ -261,7 +344,7 @@ class ApiClient implements LoggerAwareInterface
$options['max_duration'] = $maxDuration; $options['max_duration'] = $maxDuration;
} }
return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], $options); return static::$httpClient = HttpClient::createForBaseUri($config['myEpApiBaseUrl'], $options);
} }
/** /**
@@ -296,7 +379,7 @@ class ApiClient implements LoggerAwareInterface
*/ */
private function getAccessToken(array $config): AccessToken private function getAccessToken(array $config): AccessToken
{ {
if (null !== static::$accessToken && false === static::$accessToken->hasExpired()) { if (true === $this->isTokenUsable(static::$accessToken)) {
return static::$accessToken; return static::$accessToken;
} }
@@ -305,7 +388,7 @@ class ApiClient implements LoggerAwareInterface
$cachedToken = $this->getCache()->get(self::ACCESS_TOKEN_CACHE_IDENTIFIER); $cachedToken = $this->getCache()->get(self::ACCESS_TOKEN_CACHE_IDENTIFIER);
if (is_array($cachedToken) && isset($cachedToken['access_token'])) { if (is_array($cachedToken) && isset($cachedToken['access_token'])) {
$token = new AccessToken($cachedToken); $token = new AccessToken($cachedToken);
if (false === $token->hasExpired()) { if (true === $this->isTokenUsable($token)) {
static::$accessToken = $token; static::$accessToken = $token;
return static::$accessToken; return static::$accessToken;
@@ -313,7 +396,7 @@ class ApiClient implements LoggerAwareInterface
} }
try { try {
static::$accessToken = $this->getProvider($config)->getAccessToken('client_credentials'); static::$accessToken = $this->normalizeToken($this->getProvider($config)->getAccessToken('client_credentials'));
$this->cacheAccessToken(static::$accessToken); $this->cacheAccessToken(static::$accessToken);
} catch (IdentityProviderException $e) { } catch (IdentityProviderException $e) {
$message = sprintf('MyEP OAuth token request failed: %s', $e->getMessage()); $message = sprintf('MyEP OAuth token request failed: %s', $e->getMessage());
@@ -332,16 +415,53 @@ class ApiClient implements LoggerAwareInterface
return static::$accessToken; return static::$accessToken;
} }
private function cacheAccessToken(AccessToken $token): void /**
* AccessToken::hasExpired() throws for a token that reports no expiry, so it cannot be used
* as the check - a token without one is simply treated as unusable and replaced. The same
* margin as the cache lifetime is applied on read as well, so a token cannot lapse between
* this check and the API call that uses it, whichever node wrote the cache entry.
*/
private function isTokenUsable(?AccessToken $token): bool
{ {
if (null === $token) {
return false;
}
$expires = $token->getExpires(); $expires = $token->getExpires();
if (null === $expires) { if (null === $expires) {
$lifetime = self::ACCESS_TOKEN_DEFAULT_TTL; return false;
} else {
$lifetime = $expires - time() - self::ACCESS_TOKEN_EXPIRY_MARGIN;
} }
return $expires - self::ACCESS_TOKEN_EXPIRY_MARGIN > time();
}
/**
* A token that reports no expiry cannot be validated on read - isTokenUsable() rejects it
* and AccessToken::hasExpired() throws for it - so it is given the fallback lifetime here,
* once, and the rest of the class can treat every token the same way.
*/
private function normalizeToken(AccessToken $token): AccessToken
{
if (null !== $token->getExpires()) {
return $token;
}
return new AccessToken($token->jsonSerialize() + ['expires_in' => self::ACCESS_TOKEN_DEFAULT_TTL]);
}
private function discardAccessToken(): void
{
static::$accessToken = null;
$this->getCache()->remove(self::ACCESS_TOKEN_CACHE_IDENTIFIER);
}
private function cacheAccessToken(AccessToken $token): void
{
// normalizeToken() has already given a token without a reported expiry the fallback
// lifetime, so there is always a real expiry to subtract the margin from here.
$lifetime = $token->getExpires() - time() - self::ACCESS_TOKEN_EXPIRY_MARGIN;
if ($lifetime <= 0) { if ($lifetime <= 0) {
return; return;
} }
@@ -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()
@@ -4,6 +4,7 @@ return [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_badge', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_badge',
'default_sortby' => 'ORDER BY title', 'default_sortby' => 'ORDER BY title',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -18,7 +19,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'headline,subline', 'searchFields' => 'title,subtitle',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -35,6 +36,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -49,6 +51,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -4,6 +4,7 @@ return [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_city', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_city',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'label' => 'name_internal', 'label' => 'name_internal',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -40,6 +41,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -54,6 +56,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -405,6 +408,7 @@ return [
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'minitems' => 1, 'minitems' => 1,
@@ -416,6 +420,7 @@ return [
'label' => 'Gebiet', 'label' => 'Gebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_region', 'foreign_table' => 'tx_epproducts_domain_model_region',
'minitems' => 1, 'minitems' => 1,
@@ -427,6 +432,7 @@ return [
'label' => 'Officetipp', 'label' => 'Officetipp',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_officetip', 'foreign_table' => 'tx_epproducts_domain_model_officetip',
'items' => [ 'items' => [
@@ -5,6 +5,7 @@ return [
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'sortby' => 'sorting', 'sortby' => 'sorting',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -43,6 +44,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -57,6 +59,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -20,7 +20,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'code', 'searchFields' => 'room_code,room_label',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -34,6 +34,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -48,6 +49,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -145,6 +147,7 @@ return [
'label' => 'Unterkunft', 'label' => 'Unterkunft',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_hotel', 'foreign_table' => 'tx_epproducts_domain_model_hotel',
'minitems' => 1, 'minitems' => 1,
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_country', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_country',
'label' => 'name_internal', 'label' => 'name_internal',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name_internal', 'default_sortby' => 'ORDER BY name_internal',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -42,6 +43,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -56,6 +58,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -387,6 +390,7 @@ return [
'label' => 'Officetipp', 'label' => 'Officetipp',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_officetip', 'foreign_table' => 'tx_epproducts_domain_model_officetip',
'items' => [ 'items' => [
@@ -401,6 +405,7 @@ return [
'label' => 'Gebiet für Schneehöhen', 'label' => 'Gebiet für Schneehöhen',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'items' => [ 'items' => [
['Keine Zuordnung', 0] ['Keine Zuordnung', 0]
], ],
@@ -38,6 +38,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -52,6 +53,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -142,6 +144,57 @@ return [
'type' => 'passthrough', 'type' => 'passthrough',
], ],
], ],
// Denormalized columns written by DateImportService, which rebuilds this table
// wholesale. They are TEXT NOT NULL, and MySQL cannot default that, so without a
// TCA entry DataHandler omits them and every INSERT fails under STRICT_TRANS_TABLES.
'product_keywords' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'product_teaser' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'concept_popup_text' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'country_keywords' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'region_keywords' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'city_keywords' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'hotel_keywords' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'hotel_teaser' => [
'exclude' => true,
'config' => [
'type' => 'passthrough',
],
],
'bus_pro_id' => [ 'bus_pro_id' => [
'exclude' => false, 'exclude' => false,
'label' => 'BusPro ID', 'label' => 'BusPro ID',
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_fact', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_fact',
'label' => 'label', 'label' => 'label',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY label', 'default_sortby' => 'ORDER BY label',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -18,7 +19,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'name,tags,', 'searchFields' => 'name,label,keywords',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -34,6 +35,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -48,6 +50,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_faq', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_faq',
'label' => 'label', 'label' => 'label',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -35,6 +36,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -49,6 +51,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -5,6 +5,7 @@ return [
'default_sortby' => 'ORDER BY price', 'default_sortby' => 'ORDER BY price',
'sortby' => 'sorting', 'sortby' => 'sorting',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -52,6 +53,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -55,6 +55,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -5,6 +5,7 @@ return [
'default_sortby' => 'ORDER BY price', 'default_sortby' => 'ORDER BY price',
'sortby' => 'sorting', 'sortby' => 'sorting',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -53,6 +54,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -4,6 +4,7 @@ return [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_hotel', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_hotel',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getHotelLabel', 'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getHotelLabel',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -51,6 +52,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -65,6 +67,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -488,6 +491,7 @@ return [
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'minitems' => 1, 'minitems' => 1,
@@ -499,6 +503,7 @@ return [
'label' => 'Gebiet', 'label' => 'Gebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_region', 'foreign_table' => 'tx_epproducts_domain_model_region',
'minitems' => 1, 'minitems' => 1,
@@ -510,6 +515,7 @@ return [
'label' => 'Ort', 'label' => 'Ort',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_city', 'foreign_table' => 'tx_epproducts_domain_model_city',
'minitems' => 1, 'minitems' => 1,
@@ -49,12 +49,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_infobox', 'foreign_table' => 'tx_epproducts_domain_model_hotel_mapping',
'foreign_table_where' => 'AND tx_epproducts_domain_model_infobox.pid=###CURRENT_PID### AND tx_epproducts_domain_model_infobox.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_hotel_mapping.pid=###CURRENT_PID### AND tx_epproducts_domain_model_hotel_mapping.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -114,6 +115,7 @@ return [
'label' => 'Produkt', 'label' => 'Produkt',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_product', 'foreign_table' => 'tx_epproducts_domain_model_product',
'minitems' => 1, 'minitems' => 1,
@@ -4,6 +4,7 @@ return [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_infobox', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_infobox',
'default_sortby' => 'ORDER BY title', 'default_sortby' => 'ORDER BY title',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -18,7 +19,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'name', 'searchFields' => 'title,text',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -51,6 +52,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -4,6 +4,7 @@ return [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_journey', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_journey',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -33,6 +34,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -47,12 +49,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_city', 'foreign_table' => 'tx_epproducts_domain_model_journey',
'foreign_table_where' => 'AND tx_epproducts_domain_model_city.pid=###CURRENT_PID### AND tx_epproducts_domain_model_city.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_journey.pid=###CURRENT_PID### AND tx_epproducts_domain_model_journey.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_matchcode', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_matchcode',
'label' => 'code', 'label' => 'code',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -34,6 +35,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -48,6 +50,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_officetip', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_officetip',
'label' => 'label', 'label' => 'label',
'prependAtCopy' => '[COPY %s]',
'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getOfficetipLabel', 'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getOfficetipLabel',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -18,7 +19,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'text,author,product,', 'searchFields' => 'label,text,author',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -34,6 +35,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -48,6 +50,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -133,6 +136,7 @@ return [
'label' => 'Autor', 'label' => 'Autor',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_teammember', 'foreign_table' => 'tx_epproducts_domain_model_teammember',
'minitems' => 1, 'minitems' => 1,
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_product', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_product',
'label' => 'name_internal', 'label' => 'name_internal',
'prependAtCopy' => '[COPY %s]',
'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getProductLabel', 'label_userFunc' => \EP\EpProducts\Service\LabelService::class . '->getProductLabel',
'default_sortby' => 'ORDER BY name_internal', 'default_sortby' => 'ORDER BY name_internal',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
@@ -54,6 +55,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -68,6 +70,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -631,6 +634,7 @@ return [
'label' => 'Konzept', 'label' => 'Konzept',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_concept', 'foreign_table' => 'tx_epproducts_domain_model_concept',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -643,6 +647,7 @@ return [
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -655,6 +660,7 @@ return [
'label' => 'Gebiet', 'label' => 'Gebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_region', 'foreign_table' => 'tx_epproducts_domain_model_region',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -667,6 +673,7 @@ return [
'label' => 'Stadt/Ort', 'label' => 'Stadt/Ort',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_city', 'foreign_table' => 'tx_epproducts_domain_model_city',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -679,6 +686,7 @@ return [
'label' => 'Anreise', 'label' => 'Anreise',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_journey', 'foreign_table' => 'tx_epproducts_domain_model_journey',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -735,6 +743,7 @@ return [
'label' => 'Unterkunft für Belegungskalender', 'label' => 'Unterkunft für Belegungskalender',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_hotel', 'foreign_table' => 'tx_epproducts_domain_model_hotel',
'foreign_table_where' => 'AND 1=1 ORDER BY tx_epproducts_domain_model_hotel.name', 'foreign_table_where' => 'AND 1=1 ORDER BY tx_epproducts_domain_model_hotel.name',
@@ -748,6 +757,7 @@ return [
'label' => 'Officetipp', 'label' => 'Officetipp',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_officetip', 'foreign_table' => 'tx_epproducts_domain_model_officetip',
'items' => [ 'items' => [
@@ -787,6 +797,7 @@ return [
'label' => 'Produkt', 'label' => 'Produkt',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_product', 'foreign_table' => 'tx_epproducts_domain_model_product',
'foreign_table_where' => 'AND tx_epproducts_domain_model_product.uid != ###THIS_UID### ORDER BY tx_epproducts_domain_model_product.name_internal', 'foreign_table_where' => 'AND tx_epproducts_domain_model_product.uid != ###THIS_UID### ORDER BY tx_epproducts_domain_model_product.name_internal',
@@ -858,6 +869,7 @@ return [
'label' => 'Badge', 'label' => 'Badge',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_badge', 'foreign_table' => 'tx_epproducts_domain_model_badge',
'foreign_table_where' => 'AND tx_epproducts_domain_model_badge.deleted = 0 ORDER BY tx_epproducts_domain_model_badge.title', 'foreign_table_where' => 'AND tx_epproducts_domain_model_badge.deleted = 0 ORDER BY tx_epproducts_domain_model_badge.title',
@@ -873,6 +885,7 @@ return [
'label' => 'Infobox nicht buchbar', 'label' => 'Infobox nicht buchbar',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_infobox', 'foreign_table' => 'tx_epproducts_domain_model_infobox',
'foreign_table_where' => 'AND tx_epproducts_domain_model_infobox.deleted = 0 ORDER BY tx_epproducts_domain_model_infobox.title', 'foreign_table_where' => 'AND tx_epproducts_domain_model_infobox.deleted = 0 ORDER BY tx_epproducts_domain_model_infobox.title',
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_region', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_region',
'label' => 'name_internal', 'label' => 'name_internal',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name_internal', 'default_sortby' => 'ORDER BY name_internal',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -45,6 +46,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -59,6 +61,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -480,6 +483,7 @@ return [
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'minitems' => 1, 'minitems' => 1,
@@ -515,6 +519,7 @@ return [
'label' => 'Officetip', 'label' => 'Officetip',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_officetip', 'foreign_table' => 'tx_epproducts_domain_model_officetip',
'items' => [ 'items' => [
@@ -529,6 +534,7 @@ return [
'label' => 'Gebiet für Schneehöhen', 'label' => 'Gebiet für Schneehöhen',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['Keine Zuordnung', 0] ['Keine Zuordnung', 0]
@@ -544,6 +550,7 @@ return [
'label' => 'Webcam', 'label' => 'Webcam',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'items' => [ 'items' => [
['Keine Zuordnung', 0] ['Keine Zuordnung', 0]
], ],
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_reseller', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_reseller',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -32,6 +33,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -46,6 +48,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -1,141 +0,0 @@
<?php
return [
'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_roommapping',
'label' => 'code',
'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' => 'name,tags,',
'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',
'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',
'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
],
],
],
];
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_snowreport', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_snowreport',
'label' => 'region_name', 'label' => 'region_name',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY region_name', 'default_sortby' => 'ORDER BY region_name',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -18,7 +19,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'regionName,', 'searchFields' => 'region_name',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -35,6 +36,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -49,6 +51,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_staticsearchparams', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_staticsearchparams',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -35,6 +36,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -49,12 +51,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_product', 'foreign_table' => 'tx_epproducts_domain_model_staticsearchparams',
'foreign_table_where' => 'AND tx_epproducts_domain_model_product.pid=###CURRENT_PID### AND tx_epproducts_domain_model_product.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_staticsearchparams.pid=###CURRENT_PID### AND tx_epproducts_domain_model_staticsearchparams.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -156,6 +159,7 @@ return [
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -171,6 +175,7 @@ return [
'label' => 'Gebiet', 'label' => 'Gebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_region', 'foreign_table' => 'tx_epproducts_domain_model_region',
'foreign_table_where' => 'AND 1=1 ORDER BY name', 'foreign_table_where' => 'AND 1=1 ORDER BY name',
@@ -186,6 +191,7 @@ return [
'label' => 'Hotel', 'label' => 'Hotel',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_hotel', 'foreign_table' => 'tx_epproducts_domain_model_hotel',
'foreign_table_where' => 'AND 1=1 ORDER BY tx_epproducts_domain_model_hotel.name', 'foreign_table_where' => 'AND 1=1 ORDER BY tx_epproducts_domain_model_hotel.name',
@@ -4,6 +4,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_teammember', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_teammember',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'sortby' => 'sorting', 'sortby' => 'sorting',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
@@ -39,6 +40,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -53,6 +55,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
@@ -4,9 +4,11 @@ return [
'title' => 'Reiseinformationen', 'title' => 'Reiseinformationen',
'default_sortby' => 'ORDER BY title', 'default_sortby' => 'ORDER BY title',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
'origUid' => 't3_origuid',
'dividers2tabs' => true, 'dividers2tabs' => true,
'versioningWS' => true, 'versioningWS' => true,
'languageField' => 'sys_language_uid', 'languageField' => 'sys_language_uid',
@@ -18,7 +20,7 @@ return [
'starttime' => 'starttime', 'starttime' => 'starttime',
'endtime' => 'endtime', 'endtime' => 'endtime',
], ],
'searchFields' => 'name', 'searchFields' => 'title,travel_code',
'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg', 'iconfile' => 'EXT:ep_theme/Resources/Public/images/icon_ce.svg',
], ],
'types' => [ 'types' => [
@@ -55,12 +57,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_infobox', 'foreign_table' => 'tx_epproducts_domain_model_travelinfo',
'foreign_table_where' => 'AND tx_epproducts_domain_model_infobox.pid=###CURRENT_PID### AND tx_epproducts_domain_model_infobox.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_travelinfo.pid=###CURRENT_PID### AND tx_epproducts_domain_model_travelinfo.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'Reiseinfo Link', 'title' => 'Reiseinfo Link',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
@@ -51,12 +52,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_faq', 'foreign_table' => 'tx_epproducts_domain_model_travelinfolink',
'foreign_table_where' => 'AND tx_epproducts_domain_model_faq.pid=###CURRENT_PID### AND tx_epproducts_domain_model_faq.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_travelinfolink.pid=###CURRENT_PID### AND tx_epproducts_domain_model_travelinfolink.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -3,9 +3,11 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'Reiseinfo Teaser', 'title' => 'Reiseinfo Teaser',
'label' => 'title', 'label' => 'title',
'prependAtCopy' => '[COPY %s]',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
'cruser_id' => 'cruser_id', 'cruser_id' => 'cruser_id',
'origUid' => 't3_origuid',
'dividers2tabs' => true, 'dividers2tabs' => true,
'versioningWS' => true, 'versioningWS' => true,
'hideTable' => true, 'hideTable' => true,
@@ -51,12 +53,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_faq', 'foreign_table' => 'tx_epproducts_domain_model_travelinfoteaser',
'foreign_table_where' => 'AND tx_epproducts_domain_model_faq.pid=###CURRENT_PID### AND tx_epproducts_domain_model_faq.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_travelinfoteaser.pid=###CURRENT_PID### AND tx_epproducts_domain_model_travelinfoteaser.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -3,6 +3,7 @@ return [
'ctrl' => [ 'ctrl' => [
'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_webcam', 'title' => 'LLL:EXT:ep_products/Resources/Private/Language/locallang.xlf:tx_epproducts_domain_model_webcam',
'label' => 'name', 'label' => 'name',
'prependAtCopy' => '[COPY %s]',
'default_sortby' => 'ORDER BY name', 'default_sortby' => 'ORDER BY name',
'tstamp' => 'tstamp', 'tstamp' => 'tstamp',
'crdate' => 'crdate', 'crdate' => 'crdate',
@@ -33,6 +34,7 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'sys_language', 'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title', 'foreign_table_where' => 'ORDER BY sys_language.title',
@@ -47,12 +49,13 @@ return [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['', 0], ['', 0],
], ],
'foreign_table' => 'tx_epproducts_domain_model_snowreport', 'foreign_table' => 'tx_epproducts_domain_model_webcam',
'foreign_table_where' => 'AND tx_epproducts_domain_model_snowreport.pid=###CURRENT_PID### AND tx_epproducts_domain_model_snowreport.sys_language_uid IN (-1,0)', 'foreign_table_where' => 'AND tx_epproducts_domain_model_webcam.pid=###CURRENT_PID### AND tx_epproducts_domain_model_webcam.sys_language_uid IN (-1,0)',
], ],
], ],
'l10n_diffsource' => [ 'l10n_diffsource' => [
@@ -152,6 +155,7 @@ return [
'label' => 'Skigebiet', 'label' => 'Skigebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingleBox', 'renderType' => 'selectSingleBox',
'foreign_table' => 'tx_epproducts_domain_model_snowreport', 'foreign_table' => 'tx_epproducts_domain_model_snowreport',
], ],
@@ -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>
@@ -26,7 +26,6 @@ CREATE TABLE tx_epproducts_domain_model_product
external_link varchar(255) DEFAULT '' NOT NULL, external_link varchar(255) DEFAULT '' NOT NULL,
tour_link varchar(255) DEFAULT '' NOT NULL, tour_link varchar(255) DEFAULT '' NOT NULL,
path_segment varchar(255) DEFAULT '' NOT NULL, path_segment varchar(255) DEFAULT '' NOT NULL,
description text NOT NULL,
teaser text NOT NULL, teaser text NOT NULL,
teaser_long text NOT NULL, teaser_long text NOT NULL,
header_title varchar(255) DEFAULT '' NOT NULL, header_title varchar(255) DEFAULT '' NOT NULL,
@@ -495,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'
# #
@@ -1099,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)
@@ -1143,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();
}
}

Some files were not shown because too many files have changed in this diff Show More