diff --git a/composer.json b/composer.json index 2651f9a..d2c3cac 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "evomedien/vitec": "^1.0.1", "friendsoftypo3/content-blocks": "^2.4", "friendsoftypo3/headless": "^5.0@rc", + "georgringer/news": "^14.0", "netzbewegung/nb-headless-content-blocks": "^0.0.23", "typo3/cms-backend": "^14.3", "typo3/cms-belog": "^14.3", diff --git a/composer.lock b/composer.lock index e454b71..6f3a6e5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1b855a4234befa546bf9280aa9536303", + "content-hash": "894f07b60885efbdf0a5979a6879e631", "packages": [ { "name": "b13/container", @@ -364,16 +364,16 @@ }, { "name": "composer/composer", - "version": "2.10.1", + "version": "2.10.2", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "4120703b9bda8795075047b40361d7ec4d2abe49" + "reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/4120703b9bda8795075047b40361d7ec4d2abe49", - "reference": "4120703b9bda8795075047b40361d7ec4d2abe49", + "url": "https://api.github.com/repos/composer/composer/zipball/8d4439f572a97670a9edc039eb3b093cc976b4bc", + "reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc", "shasum": "" }, "require": { @@ -461,7 +461,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.10.1" + "source": "https://github.com/composer/composer/tree/2.10.2" }, "funding": [ { @@ -473,20 +473,20 @@ "type": "github" } ], - "time": "2026-06-04T08:25:59+00:00" + "time": "2026-07-01T09:24:45+00:00" }, { "name": "composer/metadata-minifier", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/composer/metadata-minifier.git", - "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + "reference": "8e86142e3ade750b837d55e55f0cdc786d8da006" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", - "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/8e86142e3ade750b837d55e55f0cdc786d8da006", + "reference": "8e86142e3ade750b837d55e55f0cdc786d8da006", "shasum": "" }, "require": { @@ -494,8 +494,8 @@ }, "require-dev": { "composer/composer": "^2", - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1", + "symfony/phpunit-bridge": "^4.2 || ^5 || ^6 || ^7" }, "type": "library", "extra": { @@ -526,7 +526,7 @@ ], "support": { "issues": "https://github.com/composer/metadata-minifier/issues", - "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + "source": "https://github.com/composer/metadata-minifier/tree/1.0.1" }, "funding": [ { @@ -536,38 +536,35 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2021-04-07T13:37:33+00:00" + "time": "2026-06-30T11:34:59+00:00" }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -605,7 +602,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -615,13 +612,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -1345,16 +1338,16 @@ }, { "name": "firebase/php-jwt", - "version": "v7.0.5", + "version": "v7.1.0", "source": { "type": "git", "url": "https://github.com/googleapis/php-jwt.git", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", "shasum": "" }, "require": { @@ -1363,6 +1356,7 @@ "require-dev": { "guzzlehttp/guzzle": "^7.4", "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", @@ -1371,7 +1365,8 @@ }, "suggest": { "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" }, "type": "library", "autoload": { @@ -1396,29 +1391,29 @@ } ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "homepage": "https://github.com/googleapis/php-jwt", "keywords": [ "jwt", "php" ], "support": { "issues": "https://github.com/googleapis/php-jwt/issues", - "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" }, - "time": "2026-04-01T20:38:03+00:00" + "time": "2026-06-11T17:54:14+00:00" }, { "name": "friendsoftypo3/content-blocks", - "version": "2.4.5", + "version": "2.4.7", "source": { "type": "git", "url": "https://github.com/FriendsOfTYPO3/content-blocks.git", - "reference": "6e82652bc653b98aabd4fc60211286c40c8abe75" + "reference": "9f2b13b5f605cd6576b9a1dec10b559c59af3c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/6e82652bc653b98aabd4fc60211286c40c8abe75", - "reference": "6e82652bc653b98aabd4fc60211286c40c8abe75", + "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/9f2b13b5f605cd6576b9a1dec10b559c59af3c66", + "reference": "9f2b13b5f605cd6576b9a1dec10b559c59af3c66", "shasum": "" }, "require": { @@ -1465,7 +1460,7 @@ "symfony/var-exporter": "" } }, - "version": "2.4.5", + "version": "2.4.7", "web-dir": ".Build/public", "extension-key": "content_blocks" } @@ -1494,7 +1489,7 @@ "issues": "https://github.com/FriendsOfTYPO3/content-blocks/issues", "source": "https://github.com/FriendsOfTYPO3/content-blocks" }, - "time": "2026-06-04T16:27:24+00:00" + "time": "2026-06-25T15:33:34+00:00" }, { "name": "friendsoftypo3/headless", @@ -1585,27 +1580,132 @@ "time": "2026-05-08T15:09:37+00:00" }, { - "name": "guzzlehttp/guzzle", - "version": "7.11.0", + "name": "georgringer/news", + "version": "14.0.3", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b" + "url": "https://github.com/georgringer/news.git", + "reference": "cec1cd776a32e5cec527575b97ca7b044ceac19c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b", + "url": "https://api.github.com/repos/georgringer/news/zipball/cec1cd776a32e5cec527575b97ca7b044ceac19c", + "reference": "cec1cd776a32e5cec527575b97ca7b044ceac19c", + "shasum": "" + }, + "require": { + "php": ">= 8.2 < 8.6", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "typo3/cms-backend": "^13.4.20 || ^14.0", + "typo3/cms-core": "^13.4.20 || ^14.0", + "typo3/cms-extbase": "^13.4.20 || ^14.0", + "typo3/cms-fluid": "^13.4.20 || ^14.0", + "typo3/cms-frontend": "^13.4.20 || ^14.0", + "typo3/cms-install": "^13.4.20 || ^14.0" + }, + "conflict": { + "sebastian/recursion-context": "6.0.0 || 6.0.1 || 6.0.2" + }, + "replace": { + "typo3-ter/news": "self.version" + }, + "require-dev": { + "a9f/typo3-fractor": "@dev", + "friendsofphp/php-cs-fixer": "^3.89.2", + "phpstan/phpdoc-parser": "^1.33.0", + "phpunit/phpunit": "^10.5.58", + "ssch/typo3-rector": "^3.7", + "typo3/cms-seo": "^13.4.20 || ^14.0", + "typo3/coding-standards": "^0.8.0", + "typo3/testing-framework": "^8.3.1", + "webmozart/assert": "^1.12.1" + }, + "suggest": { + "georgringer/news-tagsuggest": "On the fly creation of tag records within a news record", + "georgringer/numbered-pagination": "Improved pagination API", + "reelworx/rx-shariff": "GDPR compliant social sharing", + "typo3/cms-scheduler": "XML and ICS import", + "typo3/cms-seo": "Sitemaps" + }, + "type": "typo3-cms-extension", + "extra": { + "typo3/cms": { + "app-dir": ".Build", + "web-dir": ".Build/Web", + "extension-key": "news" + } + }, + "autoload": { + "psr-4": { + "GeorgRinger\\News\\": "Classes" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Georg Ringer", + "homepage": "https://ringer.it", + "role": "Developer" + } + ], + "description": "News system - Versatile news system based on Extbase & Fluid and using the latest technologies provided by TYPO3 CMS.", + "homepage": "https://extensions.typo3.org/extension/news", + "keywords": [ + "article", + "extension", + "news", + "typo3" + ], + "support": { + "docs": "https://docs.typo3.org/p/georgringer/news/main/en-us/", + "issues": "https://github.com/georgringer/news/issues", + "source": "https://github.com/georgringer/news" + }, + "funding": [ + { + "url": "https://paypal.me/GeorgRinger/10", + "type": "custom" + }, + { + "url": "https://www.amazon.de/hz/wishlist/ls/8F573K08TSDG", + "type": "custom" + }, + { + "url": "https://github.com/georgringer", + "type": "github" + }, + { + "url": "https://www.patreon.com/georgringer", + "type": "patreon" + } + ], + "time": "2026-05-18T20:53:26+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.13.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "bcd989ad36c92d42a3715379af91f2defee5b8dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/bcd989ad36c92d42a3715379af91f2defee5b8dd", + "reference": "bcd989ad36c92d42a3715379af91f2defee5b8dd", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.11", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1613,8 +1713,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.6", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1694,7 +1794,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.11.0" + "source": "https://github.com/guzzle/guzzle/tree/7.13.2" }, "funding": [ { @@ -1710,7 +1810,7 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:40:51+00:00" + "time": "2026-07-05T19:00:11+00:00" }, { "name": "guzzlehttp/promises", @@ -1798,16 +1898,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.11.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { @@ -1816,7 +1916,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1897,7 +1997,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.0" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -1913,20 +2013,20 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:30:48+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "justinrainbow/json-schema", - "version": "6.8.2", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", "shasum": "" }, "require": { @@ -1986,9 +2086,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" }, - "time": "2026-05-05T05:39:01+00:00" + "time": "2026-06-16T20:50:26+00:00" }, { "name": "lolli42/finediff", @@ -2130,16 +2230,16 @@ }, { "name": "masterminds/html5", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fcf91eb64359852f00d921887b219479b4f21251" + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", - "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", "shasum": "" }, "require": { @@ -2147,7 +2247,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { @@ -2191,9 +2291,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" }, - "time": "2025-07-25T09:04:22+00:00" + "time": "2026-06-23T18:43:15+00:00" }, { "name": "netzbewegung/nb-headless-content-blocks", @@ -2266,20 +2366,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -2318,9 +2417,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "opis/json-schema", @@ -2690,16 +2789,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.2", + "version": "2.3.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", "shasum": "" }, "require": { @@ -2731,9 +2830,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" }, - "time": "2026-01-25T14:56:51+00:00" + "time": "2026-07-08T07:01:06+00:00" }, { "name": "psr/cache", @@ -3377,16 +3476,16 @@ }, { "name": "seld/jsonlint", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", "shasum": "" }, "require": { @@ -3425,7 +3524,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" }, "funding": [ { @@ -3437,7 +3536,7 @@ "type": "tidelift" } ], - "time": "2024-07-11T14:55:45+00:00" + "time": "2026-06-12T11:32:29+00:00" }, { "name": "seld/phar-utils", @@ -3550,16 +3649,16 @@ }, { "name": "symfony/cache", - "version": "v8.0.13", + "version": "v8.0.14", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "75f92239836ce08bce4d19f2734737dae4276fe9" + "reference": "76ec84217eb39e4787d03928bba86a25cc859f76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/75f92239836ce08bce4d19f2734737dae4276fe9", - "reference": "75f92239836ce08bce4d19f2734737dae4276fe9", + "url": "https://api.github.com/repos/symfony/cache/zipball/76ec84217eb39e4787d03928bba86a25cc859f76", + "reference": "76ec84217eb39e4787d03928bba86a25cc859f76", "shasum": "" }, "require": { @@ -3625,7 +3724,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.0.13" + "source": "https://github.com/symfony/cache/tree/v8.0.14" }, "funding": [ { @@ -3645,20 +3744,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:44:11+00:00" + "time": "2026-06-17T15:00:27+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "225e8a254166bd3442e370c6f50145465db63831" + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", - "reference": "225e8a254166bd3442e370c6f50145465db63831", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", "shasum": "" }, "require": { @@ -3705,7 +3804,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" }, "funding": [ { @@ -3725,7 +3824,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:33:14+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/clock", @@ -3806,16 +3905,16 @@ }, { "name": "symfony/config", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb", + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb", "shasum": "" }, "require": { @@ -3861,7 +3960,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.10" + "source": "https://github.com/symfony/config/tree/v7.4.14" }, "funding": [ { @@ -3881,20 +3980,20 @@ "type": "tidelift" } ], - "time": "2026-05-03T14:20:49+00:00" + "time": "2026-06-09T07:51:57+00:00" }, { "name": "symfony/console", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", "shasum": "" }, "require": { @@ -3959,7 +4058,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.13" + "source": "https://github.com/symfony/console/tree/v7.4.14" }, "funding": [ { @@ -3979,20 +4078,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:56:14+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2" + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f299e20ce983be6c0744952533c6dfeaaa1448e2", - "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2c8c64a33e2e6911579e1ff79a8e06c27d48d402", + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402", "shasum": "" }, "require": { @@ -4043,7 +4142,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.13" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.14" }, "funding": [ { @@ -4063,20 +4162,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T14:07:29+00:00" + "time": "2026-06-24T07:41:05+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -4114,7 +4213,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -4134,20 +4233,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/doctrine-messenger", - "version": "v7.4.6", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-messenger.git", - "reference": "a429cd95983eaea2371ea279bed3b8a93b9ecdd3" + "reference": "c29901ea81dff6557ae3c78c0bad1d559500c2d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/a429cd95983eaea2371ea279bed3b8a93b9ecdd3", - "reference": "a429cd95983eaea2371ea279bed3b8a93b9ecdd3", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/c29901ea81dff6557ae3c78c0bad1d559500c2d8", + "reference": "c29901ea81dff6557ae3c78c0bad1d559500c2d8", "shasum": "" }, "require": { @@ -4190,7 +4289,7 @@ "description": "Symfony Doctrine Messenger Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-messenger/tree/v7.4.6" + "source": "https://github.com/symfony/doctrine-messenger/tree/v7.4.14" }, "funding": [ { @@ -4210,20 +4309,20 @@ "type": "tidelift" } ], - "time": "2026-02-18T09:40:04+00:00" + "time": "2026-06-11T07:31:44+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.0.9", + "version": "v8.0.14", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" + "reference": "3973836dd335445d0c622ffe98e418eed304d95b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3973836dd335445d0c622ffe98e418eed304d95b", + "reference": "3973836dd335445d0c622ffe98e418eed304d95b", "shasum": "" }, "require": { @@ -4275,7 +4374,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.14" }, "funding": [ { @@ -4295,20 +4394,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-06-06T11:11:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -4355,7 +4454,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -4375,20 +4474,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/expression-language", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83" + "reference": "bd5763f92959201816ecc31defdf352d2ea473be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/87ff95687748f4af65e4d5a6e917d448ec52aa83", - "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/bd5763f92959201816ecc31defdf352d2ea473be", + "reference": "bd5763f92959201816ecc31defdf352d2ea473be", "shasum": "" }, "require": { @@ -4423,7 +4522,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v7.4.8" + "source": "https://github.com/symfony/expression-language/tree/v7.4.14" }, "funding": [ { @@ -4443,7 +4542,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/filesystem", @@ -4517,16 +4616,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "13b38720174286f55d1761152b575a8d1436fc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", + "reference": "13b38720174286f55d1761152b575a8d1436fc25", "shasum": "" }, "require": { @@ -4561,7 +4660,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.14" }, "funding": [ { @@ -4581,20 +4680,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bc354f47c62301e990b7874fa662326368508e2c" + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", - "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/06db5ae1552177bf8572f8908839f12e3c06aed3", + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3", "shasum": "" }, "require": { @@ -4643,7 +4742,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.14" }, "funding": [ { @@ -4663,20 +4762,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T11:20:33+00:00" + "time": "2026-06-11T07:31:44+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.12", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f88ce03ae73e3edb5c176ce1f337709996e88495", + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495", "shasum": "" }, "require": { @@ -4727,7 +4826,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.12" + "source": "https://github.com/symfony/mailer/tree/v7.4.14" }, "funding": [ { @@ -4747,20 +4846,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-06-13T08:51:35+00:00" }, { "name": "symfony/messenger", - "version": "v7.4.12", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "906387986caecc10b2ad2e85f715d834e5133a04" + "reference": "1ebe448527c77d9efaf2d3205b23707ebaaa28c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/906387986caecc10b2ad2e85f715d834e5133a04", - "reference": "906387986caecc10b2ad2e85f715d834e5133a04", + "url": "https://api.github.com/repos/symfony/messenger/zipball/1ebe448527c77d9efaf2d3205b23707ebaaa28c4", + "reference": "1ebe448527c77d9efaf2d3205b23707ebaaa28c4", "shasum": "" }, "require": { @@ -4821,7 +4920,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v7.4.12" + "source": "https://github.com/symfony/messenger/tree/v7.4.14" }, "funding": [ { @@ -4841,7 +4940,7 @@ "type": "tidelift" } ], - "time": "2026-05-19T07:02:47+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/mime", @@ -5342,16 +5441,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -5403,7 +5502,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -5423,7 +5522,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php73", @@ -5671,16 +5770,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "8339098cae28673c15cce00d80734af0453054e2" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", - "reference": "8339098cae28673c15cce00d80734af0453054e2", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -5727,7 +5826,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -5747,7 +5846,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", @@ -6150,16 +6249,16 @@ }, { "name": "symfony/rate-limiter", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "8b162768544e5a8895c52161d63c999aca91f4a9" + "reference": "5c0af3fdc93e6115d9b806a6ea73e7de040e711d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/8b162768544e5a8895c52161d63c999aca91f4a9", - "reference": "8b162768544e5a8895c52161d63c999aca91f4a9", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/5c0af3fdc93e6115d9b806a6ea73e7de040e711d", + "reference": "5c0af3fdc93e6115d9b806a6ea73e7de040e711d", "shasum": "" }, "require": { @@ -6200,7 +6299,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.4.13" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.14" }, "funding": [ { @@ -6220,7 +6319,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/routing", @@ -6309,16 +6408,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -6372,7 +6471,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -6392,7 +6491,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/string", @@ -6486,16 +6585,16 @@ }, { "name": "symfony/translation", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "url": "https://api.github.com/repos/symfony/translation/zipball/a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", + "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", "shasum": "" }, "require": { @@ -6562,7 +6661,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.10" + "source": "https://github.com/symfony/translation/tree/v7.4.14" }, "funding": [ { @@ -6582,20 +6681,20 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:19:24+00:00" + "time": "2026-06-06T09:33:19+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", "shasum": "" }, "require": { @@ -6644,7 +6743,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" }, "funding": [ { @@ -6664,7 +6763,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/type-info", @@ -6828,16 +6927,16 @@ }, { "name": "symfony/validator", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361" + "reference": "306d904336166d001751759351d40d5e82312596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/c76458623af9a3fe3b2e5b09b36453f334c2a361", - "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361", + "url": "https://api.github.com/repos/symfony/validator/zipball/306d904336166d001751759351d40d5e82312596", + "reference": "306d904336166d001751759351d40d5e82312596", "shasum": "" }, "require": { @@ -6908,7 +7007,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.10" + "source": "https://github.com/symfony/validator/tree/v7.4.14" }, "funding": [ { @@ -6928,20 +7027,20 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:30:56+00:00" + "time": "2026-06-27T06:16:12+00:00" }, { "name": "symfony/var-exporter", - "version": "v8.0.9", + "version": "v8.0.14", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "24cf67be4dd0926e4413635418682f4fff831412" + "reference": "5185b7a7ea87a177be5fb16e8dc7756bea9b6eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/24cf67be4dd0926e4413635418682f4fff831412", - "reference": "24cf67be4dd0926e4413635418682f4fff831412", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/5185b7a7ea87a177be5fb16e8dc7756bea9b6eee", + "reference": "5185b7a7ea87a177be5fb16e8dc7756bea9b6eee", "shasum": "" }, "require": { @@ -6988,7 +7087,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.0.9" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.14" }, "funding": [ { @@ -7008,20 +7107,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-06-27T08:56:37+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" + "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f8f328665ace2370d1e10645b807ba1646dc7dcc", + "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc", "shasum": "" }, "require": { @@ -7064,7 +7163,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.13" + "source": "https://github.com/symfony/yaml/tree/v7.4.14" }, "funding": [ { @@ -7084,7 +7183,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T06:06:12+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "typo3/class-alias-loader", @@ -7151,23 +7250,23 @@ }, { "name": "typo3/cms-backend", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/backend.git", - "reference": "e3a61d3f083e10e8125f281b5c758103cfd015c4" + "reference": "f842410118ea059292dc697e3c085e5b12ea41f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/e3a61d3f083e10e8125f281b5c758103cfd015c4", - "reference": "e3a61d3f083e10e8125f281b5c758103cfd015c4", + "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/f842410118ea059292dc697e3c085e5b12ea41f0", + "reference": "f842410118ea059292dc697e3c085e5b12ea41f0", "shasum": "" }, "require": { "ext-intl": "*", "ext-libxml": "*", "psr/event-dispatcher": "^1.0", - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7240,24 +7339,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-belog", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/belog.git", - "reference": "5c0585ae27c0712b38f36225e1c564f47b7d293d" + "reference": "e5f33784c536e43929104e378d872cafbb5c3afd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/5c0585ae27c0712b38f36225e1c564f47b7d293d", - "reference": "5c0585ae27c0712b38f36225e1c564f47b7d293d", + "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/e5f33784c536e43929104e378d872cafbb5c3afd", + "reference": "e5f33784c536e43929104e378d872cafbb5c3afd", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7307,24 +7406,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-beuser", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/beuser.git", - "reference": "7269663f07577c1a17674207c8aff5858adf9a20" + "reference": "124498b0bcf090e1cde666f9f6f80d34322bdd1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/7269663f07577c1a17674207c8aff5858adf9a20", - "reference": "7269663f07577c1a17674207c8aff5858adf9a20", + "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/124498b0bcf090e1cde666f9f6f80d34322bdd1e", + "reference": "124498b0bcf090e1cde666f9f6f80d34322bdd1e", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7374,7 +7473,7 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-cli", @@ -7483,22 +7582,23 @@ }, { "name": "typo3/cms-core", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/core.git", - "reference": "e323e061321a6a8441d2b552dfe59af4de478d2c" + "reference": "e59ef7a3348e439bf103140e01614f1bc403d373" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/e323e061321a6a8441d2b552dfe59af4de478d2c", - "reference": "e323e061321a6a8441d2b552dfe59af4de478d2c", + "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/e59ef7a3348e439bf103140e01614f1bc403d373", + "reference": "e59ef7a3348e439bf103140e01614f1bc403d373", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.1.1", "christian-riesen/base32": "^1.6", "composer-runtime-api": "^2.1", + "composer/class-map-generator": "^1.7.2", "composer/semver": "^3.4", "doctrine/dbal": "~4.4.3", "doctrine/lexer": "^3.0.1", @@ -7537,7 +7637,7 @@ "symfony/expression-language": "^7.4.8", "symfony/filesystem": "^7.4.8", "symfony/finder": "^7.4.8", - "symfony/http-foundation": "^7.4.8", + "symfony/http-foundation": "^7.4.13", "symfony/mailer": "^7.4.12", "symfony/messenger": "^7.4.8", "symfony/mime": "^7.4.12", @@ -7545,14 +7645,14 @@ "symfony/polyfill-php83": "^1.36.0", "symfony/process": "^7.4.8", "symfony/rate-limiter": "^7.4.8", - "symfony/routing": "^7.4.12", + "symfony/routing": "^7.4.13", "symfony/translation": "^7.4.8", "symfony/uid": "^7.4.8", "symfony/yaml": "^7.4.12", "typo3/class-alias-loader": "^2.0.1", "typo3/cms-cli": "^3.1.3", "typo3/cms-composer-installers": "^5.0.2", - "typo3/html-sanitizer": "^2.2.0", + "typo3/html-sanitizer": "^2.3.2", "typo3fluid/fluid": "^5.3.1" }, "conflict": { @@ -7637,28 +7737,28 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-dashboard", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/dashboard.git", - "reference": "195f573f0c44c239de385fb712969ea7455c7d7b" + "reference": "0dfdb48e21dfd7e48785b775aa0265c981ca43b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/195f573f0c44c239de385fb712969ea7455c7d7b", - "reference": "195f573f0c44c239de385fb712969ea7455c7d7b", + "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/0dfdb48e21dfd7e48785b775aa0265c981ca43b1", + "reference": "0dfdb48e21dfd7e48785b775aa0265c981ca43b1", "shasum": "" }, "require": { - "typo3/cms-backend": "14.3.2", - "typo3/cms-core": "14.3.2", - "typo3/cms-extbase": "14.3.2", - "typo3/cms-fluid": "14.3.2", - "typo3/cms-frontend": "14.3.2" + "typo3/cms-backend": "14.3.4", + "typo3/cms-core": "14.3.4", + "typo3/cms-extbase": "14.3.4", + "typo3/cms-fluid": "14.3.4", + "typo3/cms-frontend": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7709,20 +7809,20 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-extbase", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extbase.git", - "reference": "c178b1246599c6619452b5ecbad980bc1a9cd9a1" + "reference": "3d41793cdb2dcae4a3490f859af6a999e5cc5627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/c178b1246599c6619452b5ecbad980bc1a9cd9a1", - "reference": "c178b1246599c6619452b5ecbad980bc1a9cd9a1", + "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/3d41793cdb2dcae4a3490f859af6a999e5cc5627", + "reference": "3d41793cdb2dcae4a3490f859af6a999e5cc5627", "shasum": "" }, "require": { @@ -7734,7 +7834,7 @@ "symfony/property-access": "^7.4.8", "symfony/property-info": "^7.4.8", "symfony/validator": "^7.4.8", - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7795,25 +7895,25 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-extensionmanager", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extensionmanager.git", - "reference": "613d92bb63eeaa6611fa8b426fa50282386d7614" + "reference": "a3f272a35cc3938324f5b9fc7a8131ed5f731851" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/613d92bb63eeaa6611fa8b426fa50282386d7614", - "reference": "613d92bb63eeaa6611fa8b426fa50282386d7614", + "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/a3f272a35cc3938324f5b9fc7a8131ed5f731851", + "reference": "a3f272a35cc3938324f5b9fc7a8131ed5f731851", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7865,24 +7965,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-felogin", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/felogin.git", - "reference": "c44e401f138491c494018390ccae58aa9c7d48b9" + "reference": "d45a4987e58186b3806829ca0e715b148ea6b721" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/c44e401f138491c494018390ccae58aa9c7d48b9", - "reference": "c44e401f138491c494018390ccae58aa9c7d48b9", + "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/d45a4987e58186b3806829ca0e715b148ea6b721", + "reference": "d45a4987e58186b3806829ca0e715b148ea6b721", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -7932,24 +8032,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-filelist", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/filelist.git", - "reference": "e4f20c9aa130a2d23e3de5bc904877ca07b071a0" + "reference": "8bc262bbd3577a5535317941e141b0975803a4ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/e4f20c9aa130a2d23e3de5bc904877ca07b071a0", - "reference": "e4f20c9aa130a2d23e3de5bc904877ca07b071a0", + "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/8bc262bbd3577a5535317941e141b0975803a4ca", + "reference": "8bc262bbd3577a5535317941e141b0975803a4ca", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8001,27 +8101,27 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-fluid", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid.git", - "reference": "c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95" + "reference": "8f50a943ac2a22532379591c97b570119c8e4bae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95", - "reference": "c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/8f50a943ac2a22532379591c97b570119c8e4bae", + "reference": "8f50a943ac2a22532379591c97b570119c8e4bae", "shasum": "" }, "require": { "symfony/dependency-injection": "^7.4.8", "symfony/finder": "^7.4.8", - "typo3/cms-core": "14.3.2", - "typo3/cms-extbase": "14.3.2", + "typo3/cms-core": "14.3.4", + "typo3/cms-extbase": "14.3.4", "typo3fluid/fluid": "^5.3.1" }, "conflict": { @@ -8075,26 +8175,26 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-fluid-styled-content", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid_styled_content.git", - "reference": "a3c108e57adec1fa71c8d29420eb0205bf090185" + "reference": "b08e119753ab90e15f015135cdfd65d47bbdbbf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/a3c108e57adec1fa71c8d29420eb0205bf090185", - "reference": "a3c108e57adec1fa71c8d29420eb0205bf090185", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/b08e119753ab90e15f015135cdfd65d47bbdbbf6", + "reference": "b08e119753ab90e15f015135cdfd65d47bbdbbf6", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2", - "typo3/cms-fluid": "14.3.2", - "typo3/cms-frontend": "14.3.2" + "typo3/cms-core": "14.3.4", + "typo3/cms-fluid": "14.3.4", + "typo3/cms-frontend": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8144,27 +8244,27 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-form", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/form.git", - "reference": "5d4d429f6870582b6debfee339fbf39891ed81f8" + "reference": "3307aae5867a885cd239226c9dda2791ad711f56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/5d4d429f6870582b6debfee339fbf39891ed81f8", - "reference": "5d4d429f6870582b6debfee339fbf39891ed81f8", + "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/3307aae5867a885cd239226c9dda2791ad711f56", + "reference": "3307aae5867a885cd239226c9dda2791ad711f56", "shasum": "" }, "require": { "psr/http-message": "^1.1 || ^2.0", "symfony/expression-language": "^7.4.8", - "typo3/cms-core": "14.3.2", - "typo3/cms-frontend": "14.3.2" + "typo3/cms-core": "14.3.4", + "typo3/cms-frontend": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8219,25 +8319,25 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-frontend", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/frontend.git", - "reference": "9a623a68e880bbf5ef468b4c1206648271dfe58a" + "reference": "bd865d76c5b63a5c0b1f0325b32292e975ad7c38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/9a623a68e880bbf5ef468b4c1206648271dfe58a", - "reference": "9a623a68e880bbf5ef468b4c1206648271dfe58a", + "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/bd865d76c5b63a5c0b1f0325b32292e975ad7c38", + "reference": "bd865d76c5b63a5c0b1f0325b32292e975ad7c38", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8297,24 +8397,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-impexp", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/impexp.git", - "reference": "9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e" + "reference": "b7644c24e3cb118a91be1eafa8810eb33cd94ddf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e", - "reference": "9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e", + "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/b7644c24e3cb118a91be1eafa8810eb33cd94ddf", + "reference": "b7644c24e3cb118a91be1eafa8810eb33cd94ddf", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8364,24 +8464,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-info", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/info.git", - "reference": "d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd" + "reference": "b530430877339b2a16c30ba7b78b8c88454dd7a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd", - "reference": "d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd", + "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/b530430877339b2a16c30ba7b78b8c88454dd7a1", + "reference": "b530430877339b2a16c30ba7b78b8c88454dd7a1", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8434,20 +8534,20 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-install", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/install.git", - "reference": "033b815b778af0a6bd1a55fd8a8755dc7f42d9e4" + "reference": "a4c5bd2e9a598d5d45975312ae097df2a33ef05d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/033b815b778af0a6bd1a55fd8a8755dc7f42d9e4", - "reference": "033b815b778af0a6bd1a55fd8a8755dc7f42d9e4", + "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/a4c5bd2e9a598d5d45975312ae097df2a33ef05d", + "reference": "a4c5bd2e9a598d5d45975312ae097df2a33ef05d", "shasum": "" }, "require": { @@ -8455,10 +8555,10 @@ "guzzlehttp/promises": "^2.0.3", "nikic/php-parser": "^5.4.0", "symfony/finder": "^7.4.8", - "symfony/http-foundation": "^7.4.8", - "typo3/cms-core": "14.3.2", - "typo3/cms-extbase": "14.3.2", - "typo3/cms-fluid": "14.3.2" + "symfony/http-foundation": "^7.4.13", + "typo3/cms-core": "14.3.4", + "typo3/cms-extbase": "14.3.4", + "typo3/cms-fluid": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8511,24 +8611,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-reactions", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/reactions.git", - "reference": "5e4215eabe684343da7ed2eeae472680cefc9c1d" + "reference": "700233067b941381d551fa8a0662c92977ba9ceb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/5e4215eabe684343da7ed2eeae472680cefc9c1d", - "reference": "5e4215eabe684343da7ed2eeae472680cefc9c1d", + "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/700233067b941381d551fa8a0662c92977ba9ceb", + "reference": "700233067b941381d551fa8a0662c92977ba9ceb", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8578,24 +8678,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-recycler", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/recycler.git", - "reference": "d17e66c3bf8819b4915a582efe510636553ccfaa" + "reference": "e34ad4cf0561f2e441248c52243457adef3a9bac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/d17e66c3bf8819b4915a582efe510636553ccfaa", - "reference": "d17e66c3bf8819b4915a582efe510636553ccfaa", + "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/e34ad4cf0561f2e441248c52243457adef3a9bac", + "reference": "e34ad4cf0561f2e441248c52243457adef3a9bac", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8648,24 +8748,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-rte-ckeditor", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/rte_ckeditor.git", - "reference": "f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0" + "reference": "1e649567cec0c67a3acef5fc2a680f861c6ed146" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0", - "reference": "f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0", + "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/1e649567cec0c67a3acef5fc2a680f861c6ed146", + "reference": "1e649567cec0c67a3acef5fc2a680f861c6ed146", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8715,24 +8815,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-scheduler", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/scheduler.git", - "reference": "398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850" + "reference": "b5cb0dfbd2e52fc6903cc9e695d7536e55489321" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850", - "reference": "398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850", + "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/b5cb0dfbd2e52fc6903cc9e695d7536e55489321", + "reference": "b5cb0dfbd2e52fc6903cc9e695d7536e55489321", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8779,26 +8879,26 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-seo", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/seo.git", - "reference": "db45e6c092197c32596a9b4e59123784cf442b42" + "reference": "ed5e9f465e1379adfcff0abedbfe1d68b96f6bdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/db45e6c092197c32596a9b4e59123784cf442b42", - "reference": "db45e6c092197c32596a9b4e59123784cf442b42", + "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/ed5e9f465e1379adfcff0abedbfe1d68b96f6bdc", + "reference": "ed5e9f465e1379adfcff0abedbfe1d68b96f6bdc", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2", - "typo3/cms-extbase": "14.3.2", - "typo3/cms-frontend": "14.3.2" + "typo3/cms-core": "14.3.4", + "typo3/cms-extbase": "14.3.4", + "typo3/cms-frontend": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8851,24 +8951,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-sys-note", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/sys_note.git", - "reference": "e16ca23b306d844997dc97998cbb7c77d7624f9a" + "reference": "14543f7367fb92adbacc96a889ff602fe559a979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/e16ca23b306d844997dc97998cbb7c77d7624f9a", - "reference": "e16ca23b306d844997dc97998cbb7c77d7624f9a", + "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/14543f7367fb92adbacc96a889ff602fe559a979", + "reference": "14543f7367fb92adbacc96a889ff602fe559a979", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8921,24 +9021,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-tstemplate", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/tstemplate.git", - "reference": "e3f09b0da26a57e25a07223253743f009b6c51ab" + "reference": "caa2a8e1a373a8ae5b8cba0ea8e8944bffce83f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/e3f09b0da26a57e25a07223253743f009b6c51ab", - "reference": "e3f09b0da26a57e25a07223253743f009b6c51ab", + "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/caa2a8e1a373a8ae5b8cba0ea8e8944bffce83f4", + "reference": "caa2a8e1a373a8ae5b8cba0ea8e8944bffce83f4", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -8988,24 +9088,24 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-viewpage", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/viewpage.git", - "reference": "b766123bdb09d627b5ad4bef09bf2aa401243927" + "reference": "519ebbb6b6da69b13bd2a7a3651c0ae1e8ece3ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/b766123bdb09d627b5ad4bef09bf2aa401243927", - "reference": "b766123bdb09d627b5ad4bef09bf2aa401243927", + "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/519ebbb6b6da69b13bd2a7a3651c0ae1e8ece3ee", + "reference": "519ebbb6b6da69b13bd2a7a3651c0ae1e8ece3ee", "shasum": "" }, "require": { - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -9055,25 +9155,25 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/cms-webhooks", - "version": "v14.3.2", + "version": "v14.3.4", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/webhooks.git", - "reference": "2abedd921aac6a228c94eec6d0d68f5bd3769dc0" + "reference": "e72dac3ec0378c4f5094987ffdc08ba07f68cb41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/2abedd921aac6a228c94eec6d0d68f5bd3769dc0", - "reference": "2abedd921aac6a228c94eec6d0d68f5bd3769dc0", + "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/e72dac3ec0378c4f5094987ffdc08ba07f68cb41", + "reference": "e72dac3ec0378c4f5094987ffdc08ba07f68cb41", "shasum": "" }, "require": { "symfony/uid": "^7.4.8", - "typo3/cms-core": "14.3.2" + "typo3/cms-core": "14.3.4" }, "conflict": { "typo3/cms": "*" @@ -9123,20 +9223,20 @@ "type": "membership" } ], - "time": "2026-05-26T09:37:47+00:00" + "time": "2026-06-16T08:52:34+00:00" }, { "name": "typo3/html-sanitizer", - "version": "v2.3.1", + "version": "v2.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3/html-sanitizer.git", - "reference": "988caa31b5fa0dbe17a8331bfa3245898a650d88" + "reference": "8b5d0be44ded457ca993ec9ca93d859941c63764" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/html-sanitizer/zipball/988caa31b5fa0dbe17a8331bfa3245898a650d88", - "reference": "988caa31b5fa0dbe17a8331bfa3245898a650d88", + "url": "https://api.github.com/repos/TYPO3/html-sanitizer/zipball/8b5d0be44ded457ca993ec9ca93d859941c63764", + "reference": "8b5d0be44ded457ca993ec9ca93d859941c63764", "shasum": "" }, "require": { @@ -9172,9 +9272,9 @@ "description": "HTML sanitizer aiming to provide XSS-safe markup based on explicitly allowed tags, attributes and values.", "support": { "issues": "https://github.com/TYPO3/html-sanitizer/issues", - "source": "https://github.com/TYPO3/html-sanitizer/tree/v2.3.1" + "source": "https://github.com/TYPO3/html-sanitizer/tree/v2.3.2" }, - "time": "2026-04-30T11:50:45+00:00" + "time": "2026-06-08T15:19:36+00:00" }, { "name": "typo3fluid/fluid", @@ -9238,16 +9338,16 @@ }, { "name": "webmozart/assert", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { @@ -9298,9 +9398,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.4.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "time": "2026-05-20T13:07:01+00:00" + "time": "2026-06-15T15:31:57+00:00" } ], "packages-dev": [], diff --git a/config/sites/vitec/config.yaml b/config/sites/vitec/config.yaml index 070e13f..9c99dfe 100644 --- a/config/sites/vitec/config.yaml +++ b/config/sites/vitec/config.yaml @@ -15,6 +15,11 @@ dependencies: - vitec/card - vitec/intro-paragraph - vitec/video + - vitec/faq + - georgringer/news + - georgringer/news-sitemap + - georgringer/news-recordlinks + - vitec/columns frontendBase: '' headless: 1 languages: @@ -49,4 +54,19 @@ routeEnhancers: tableName: tx_vitec_domain_model_product routeFieldName: slug routeValuePrefix: '' + NewsDetail: + type: Extbase + limitToPages: + - 21 + extension: News + plugin: Pi1 + routes: + - routePath: '/{news-title}' + _controller: 'News::detail' + _arguments: + news-title: news + defaultController: 'News::detail' + aspects: + news-title: + type: NewsTitle websiteTitle: '' diff --git a/fix-asset-perms.sh b/fix-asset-perms.sh new file mode 100644 index 0000000..03a17c2 --- /dev/null +++ b/fix-asset-perms.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# ============================================================================= +# VITEC — fix backend asset permissions after a deploy via the Windows sshfs mount +# ----------------------------------------------------------------------------- +# sshfs-win (WinFsp) writes newly deployed files with mode 770 (no other-read). +# The webserver runs as "other" (suexec) -> assets under Resources/Public and +# public/_assets return HTTP 403 (backend icons / CSS / JS missing). +# +# chmod OVER the Windows mount does NOT change the real server mode, so this +# has to run ON THE SERVER, via real SSH, as the site owner (vitecevo). +# +# Usage (from the live/ project root on the server): +# bash fix-asset-perms.sh +# ============================================================================= +set -u +cd "$(dirname "$0")" || exit 1 + +echo "[perms] Project root: $(pwd)" +echo "[perms] Setting files 644 / directories 755 under Resources/Public + _assets ..." +find public/_assets packages/vitec/Resources/Public -type f -exec chmod 644 {} \; 2>/dev/null +find public/_assets packages/vitec/Resources/Public -type d -exec chmod 755 {} \; 2>/dev/null + +echo "[perms] Flushing TYPO3 caches ..." +if [ -x vendor/bin/typo3 ]; then + vendor/bin/typo3 cache:flush +else + echo "[perms] (vendor/bin/typo3 not found — skip cache flush)" +fi + +echo "[perms] Done." diff --git a/packages/vitec/Classes/Backend/FormEngine/StructuredDataGeneratorWizard.php b/packages/vitec/Classes/Backend/FormEngine/StructuredDataGeneratorWizard.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Controller/Backend/StructuredDataController.php b/packages/vitec/Classes/Controller/Backend/StructuredDataController.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Controller/EventController.php b/packages/vitec/Classes/Controller/EventController.php new file mode 100644 index 0000000..fc3faae --- /dev/null +++ b/packages/vitec/Classes/Controller/EventController.php @@ -0,0 +1,39 @@ +eventRepository = $eventRepository; + } + + public function listAction(): ResponseInterface + { + $this->view->assign('events', $this->eventRepository->findUpcoming()); + return $this->htmlResponse(); + } +} diff --git a/packages/vitec/Classes/DataProcessing/.msys00000000000000004cda359073ead130 b/packages/vitec/Classes/DataProcessing/.msys00000000000000004cda359073ead130 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/DataProcessing/.msys00000000000000004dd18ef4cfaf2af9 b/packages/vitec/Classes/DataProcessing/.msys00000000000000004dd18ef4cfaf2af9 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php old mode 100755 new mode 100644 index 380ab01..ee54d3f --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -12,6 +12,8 @@ use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; +use Evomedien\Vitec\UserFunc\EventlistJsonRenderer; +use Evomedien\Vitec\UserFunc\NewsJsonRenderer; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -20,9 +22,13 @@ use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; /** * Collect container children grouped by colPos and emit a lean, transport- - * ready structure. Each child is normalised to: - * { id, type, colPos, sorting, appearance, data } - * `data` only contains non-empty content-relevant fields. + * ready structure. Each column is normalised to: + * { config: { colPos, align, justify }, contentElements: [ { id, type, … } ] } + * `align`/`justify` are the PARENT container's per-column flex settings + * (tx_vitec_col{N}_align/justify) and therefore apply to ALL children of that + * column. Each child is normalised to { id, type, colPos, sorting, appearance, + * data }, where `data` only contains non-empty content-relevant fields — + * container-level fields (gap, col flex) are never leaked to children. * * VITEC list-plugins nested as container children (productlist, productshow, * usecaselist, usecaseshow) are resolved to the SAME JSON the top-level @@ -59,6 +65,30 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'uploads_description', 'uploads_type', ]; + /** + * Container-level fields — they belong on the parent container (grid gap + * and per-column flex) and must never appear in a child's `data`. + */ + private const CONTAINER_FIELDS = [ + 'tx_vitec_gap', + 'tx_vitec_col1_align', 'tx_vitec_col1_justify', + 'tx_vitec_col2_align', 'tx_vitec_col2_justify', + 'tx_vitec_col3_align', 'tx_vitec_col3_justify', + 'tx_vitec_col4_align', 'tx_vitec_col4_justify', + ]; + + /** + * Per container CType: colPos value => 1-based column number. Used to pick + * the parent's tx_vitec_col{N}_align/justify for each rendered column. + */ + private const COLPOS_TO_COLUMN = [ + 'vitec_cols_50_50' => [211 => 1, 212 => 2], + 'vitec_cols_33_66' => [251 => 1, 252 => 2], + 'vitec_cols_66_33' => [241 => 1, 242 => 2], + 'vitec_cols_33_33_33' => [221 => 1, 222 => 2, 223 => 3], + 'vitec_cols_25_25_25_25' => [231 => 1, 232 => 2, 233 => 3, 234 => 4], + ]; + /** Fields whose 0/empty value is still meaningful. */ private const KEEP_IF_ZERO = [ 'header_layout', @@ -80,6 +110,16 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], + 'vitec_eventlist' => [EventlistJsonRenderer::class, 'eventlist'], + 'news_pi1' => [NewsJsonRenderer::class, 'news'], + 'news_newsliststicky' => [NewsJsonRenderer::class, 'news'], + 'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'], + 'news_newsdetail' => [NewsJsonRenderer::class, 'news'], + 'news_newsdatemenu' => [NewsJsonRenderer::class, 'news'], + 'news_categorylist' => [NewsJsonRenderer::class, 'news'], + 'news_newssearchform' => [NewsJsonRenderer::class, 'news'], + 'news_newssearchresult' => [NewsJsonRenderer::class, 'news'], + 'news_taglist' => [NewsJsonRenderer::class, 'news'], ]; public function process( @@ -121,10 +161,20 @@ final class ContainerChildrenProcessor implements DataProcessorInterface } ksort($byColPos); + // Per-column flex settings come from the parent container record. + $parentCType = (string)($cObj->data['CType'] ?? ''); + $colMap = self::COLPOS_TO_COLUMN[$parentCType] ?? []; + $items = []; foreach ($byColPos as $colPos => $contentElements) { + $config = ['colPos' => $colPos]; + $columnNo = $colMap[$colPos] ?? null; + if ($columnNo !== null) { + $config['align'] = (string)($cObj->data['tx_vitec_col' . $columnNo . '_align'] ?? 'stretch'); + $config['justify'] = (string)($cObj->data['tx_vitec_col' . $columnNo . '_justify'] ?? 'flex-start'); + } $items[] = [ - 'config' => ['colPos' => $colPos], + 'config' => $config, 'contentElements' => $contentElements, ]; } @@ -147,6 +197,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface if (in_array($field, self::SYSTEM_FIELDS, true)) { continue; } + if (in_array($field, self::CONTAINER_FIELDS, true)) { + continue; + } if ($this->isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { continue; } diff --git a/packages/vitec/Classes/Domain/Model/Event.php b/packages/vitec/Classes/Domain/Model/Event.php new file mode 100644 index 0000000..c0ff97c --- /dev/null +++ b/packages/vitec/Classes/Domain/Model/Event.php @@ -0,0 +1,266 @@ + + * @TYPO3\CMS\Extbase\Annotation\ORM\Lazy + */ + protected $categories; + + public function __construct() + { + $this->categories = new ObjectStorage(); + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getSlug(): string + { + return $this->slug; + } + + public function setSlug(string $slug): void + { + $this->slug = $slug; + } + + public function getTeaser(): string + { + return $this->teaser; + } + + public function setTeaser(string $teaser): void + { + $this->teaser = $teaser; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getEventstart(): ?\DateTime + { + return $this->eventstart; + } + + public function setEventstart(?\DateTime $eventstart): void + { + $this->eventstart = $eventstart; + } + + public function getEventend(): ?\DateTime + { + return $this->eventend; + } + + public function setEventend(?\DateTime $eventend): void + { + $this->eventend = $eventend; + } + + public function getVenue(): string + { + return $this->venue; + } + + public function setVenue(string $venue): void + { + $this->venue = $venue; + } + + public function getBooth(): string + { + return $this->booth; + } + + public function setBooth(string $booth): void + { + $this->booth = $booth; + } + + public function getCity(): string + { + return $this->city; + } + + public function setCity(string $city): void + { + $this->city = $city; + } + + public function getCountry(): string + { + return $this->country; + } + + public function setCountry(string $country): void + { + $this->country = $country; + } + + public function getAttendancemode(): string + { + return $this->attendancemode; + } + + public function setAttendancemode(string $attendancemode): void + { + $this->attendancemode = $attendancemode; + } + + public function getEventstatus(): string + { + return $this->eventstatus; + } + + public function setEventstatus(string $eventstatus): void + { + $this->eventstatus = $eventstatus; + } + + public function getEventurl(): string + { + return $this->eventurl; + } + + public function setEventurl(string $eventurl): void + { + $this->eventurl = $eventurl; + } + + public function getMeetinglink(): string + { + return $this->meetinglink; + } + + public function setMeetinglink(string $meetinglink): void + { + $this->meetinglink = $meetinglink; + } + + public function getImage(): ?FileReference + { + return $this->image; + } + + public function setImage(?FileReference $image): void + { + $this->image = $image; + } + + public function getHideonwebsite(): bool + { + return $this->hideonwebsite; + } + + public function setHideonwebsite(bool $hideonwebsite): void + { + $this->hideonwebsite = $hideonwebsite; + } + + public function getHideonapp(): bool + { + return $this->hideonapp; + } + + public function setHideonapp(bool $hideonapp): void + { + $this->hideonapp = $hideonapp; + } + + public function addCategory(Category $category): void + { + $this->categories->attach($category); + } + + public function removeCategory(Category $categoryToRemove): void + { + $this->categories->detach($categoryToRemove); + } + + public function getCategories(): ObjectStorage + { + return $this->categories; + } + + public function setCategories(ObjectStorage $categories): void + { + $this->categories = $categories; + } +} diff --git a/packages/vitec/Classes/Domain/Model/Product.php b/packages/vitec/Classes/Domain/Model/Product.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Domain/Repository/EventRepository.php b/packages/vitec/Classes/Domain/Repository/EventRepository.php new file mode 100644 index 0000000..e768628 --- /dev/null +++ b/packages/vitec/Classes/Domain/Repository/EventRepository.php @@ -0,0 +1,71 @@ + QueryInterface::ORDER_ASCENDING, + ]; + + /** + * All upcoming (or currently running) events, soonest first. + * "Upcoming" = event end (or start, when no end is set) is today or later. + */ + public function findUpcoming(): QueryResultInterface + { + $todayMidnight = strtotime('today'); + + $query = $this->createQuery(); + $query->getQuerySettings()->setRespectStoragePage(false); + $query->matching( + $query->logicalAnd( + $query->equals('hideonwebsite', 0), + $query->logicalOr( + $query->greaterThanOrEqual('eventend', $todayMidnight), + $query->logicalAnd( + $query->equals('eventend', 0), + $query->greaterThanOrEqual('eventstart', $todayMidnight) + ) + ) + ) + ); + return $query->execute(); + } + + /** + * Past events, most recent first. + */ + public function findPast(): QueryResultInterface + { + $todayMidnight = strtotime('today'); + + $query = $this->createQuery(); + $query->getQuerySettings()->setRespectStoragePage(false); + $query->matching( + $query->logicalAnd( + $query->equals('hideonwebsite', 0), + $query->logicalOr( + $query->logicalAnd( + $query->greaterThan('eventend', 0), + $query->lessThan('eventend', $todayMidnight) + ), + $query->logicalAnd( + $query->equals('eventend', 0), + $query->greaterThan('eventstart', 0), + $query->lessThan('eventstart', $todayMidnight) + ) + ) + ) + ); + $query->setOrderings(['eventstart' => QueryInterface::ORDER_DESCENDING]); + return $query->execute(); + } +} diff --git a/packages/vitec/Classes/EventListener/LoadContainerCollapseAssets.php b/packages/vitec/Classes/EventListener/LoadContainerCollapseAssets.php new file mode 100644 index 0000000..9af2542 --- /dev/null +++ b/packages/vitec/Classes/EventListener/LoadContainerCollapseAssets.php @@ -0,0 +1,27 @@ +loadJavaScriptModule('@evomedien/vitec/container-collapse.js'); + $pageRenderer->addCssFile( + 'EXT:vitec/Resources/Public/Css/backend-container-collapse.css' + ); + } +} diff --git a/packages/vitec/Classes/EventListener/ShowFrontendLayoutBanner.php b/packages/vitec/Classes/EventListener/ShowFrontendLayoutBanner.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Hook/SyncBackendLayoutHook.php b/packages/vitec/Classes/Hook/SyncBackendLayoutHook.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys00000000000000002272de42bf87f8e5 b/packages/vitec/Classes/Service/.msys00000000000000002272de42bf87f8e5 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys0000000000000000274fb505bf117200 b/packages/vitec/Classes/Service/.msys0000000000000000274fb505bf117200 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys00000000000000002751b67b19a9c636 b/packages/vitec/Classes/Service/.msys00000000000000002751b67b19a9c636 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys00000000000000002752b73c4a27d4ba b/packages/vitec/Classes/Service/.msys00000000000000002752b73c4a27d4ba old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys00000000000000002753b7f777b58735 b/packages/vitec/Classes/Service/.msys00000000000000002753b7f777b58735 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys00000000000000002753b7f978b2a67b b/packages/vitec/Classes/Service/.msys00000000000000002753b7f978b2a67b old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/.msys0000000000000000284b115dd4770044 b/packages/vitec/Classes/Service/.msys0000000000000000284b115dd4770044 old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/Service/ContentElementResolver.php b/packages/vitec/Classes/Service/ContentElementResolver.php old mode 100755 new mode 100644 index 61c7b96..c2b9d24 --- a/packages/vitec/Classes/Service/ContentElementResolver.php +++ b/packages/vitec/Classes/Service/ContentElementResolver.php @@ -14,6 +14,8 @@ use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; +use Evomedien\Vitec\UserFunc\EventlistJsonRenderer; +use Evomedien\Vitec\UserFunc\NewsJsonRenderer; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -73,6 +75,16 @@ final class ContentElementResolver 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], + 'vitec_eventlist' => [EventlistJsonRenderer::class, 'eventlist'], + 'news_pi1' => [NewsJsonRenderer::class, 'news'], + 'news_newsliststicky' => [NewsJsonRenderer::class, 'news'], + 'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'], + 'news_newsdetail' => [NewsJsonRenderer::class, 'news'], + 'news_newsdatemenu' => [NewsJsonRenderer::class, 'news'], + 'news_categorylist' => [NewsJsonRenderer::class, 'news'], + 'news_newssearchform' => [NewsJsonRenderer::class, 'news'], + 'news_newssearchresult' => [NewsJsonRenderer::class, 'news'], + 'news_taglist' => [NewsJsonRenderer::class, 'news'], ]; /** diff --git a/packages/vitec/Classes/Service/StructuredDataService.php b/packages/vitec/Classes/Service/StructuredDataService.php old mode 100755 new mode 100644 index 9793c31..3f852cc --- a/packages/vitec/Classes/Service/StructuredDataService.php +++ b/packages/vitec/Classes/Service/StructuredDataService.php @@ -305,6 +305,148 @@ final class StructuredDataService return $video; } + /** + * schema.org FAQPage from a list of question/answer pairs. + * Answers may contain RTE HTML — tags are stripped for the JSON-LD text. + * + * @param list $items + */ + public function buildFaqPage(array $items, string $base): ?array + { + $mainEntity = []; + foreach ($items as $item) { + $question = trim((string)($item['question'] ?? '')); + $answer = trim(strip_tags((string)($item['answer'] ?? ''))); + if ($question === '' || $answer === '') { + continue; + } + $mainEntity[] = [ + '@type' => 'Question', + 'name' => $question, + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => $answer, + ], + ]; + } + + if ($mainEntity === []) { + return null; + } + + return [ + '@type' => 'FAQPage', + 'mainEntity' => $mainEntity, + ]; + } + + /** + * schema.org ExhibitionEvent (trade-show) node from a + * tx_vitec_domain_model_event row. + * + * Google rich-result requirements: name, startDate, location. + * Recommended extras handled here: endDate, description, image, url, + * eventStatus, eventAttendanceMode, organizer. + * + * @param array $e Event DB row + * @param list $imageUrls Absolute or root-relative logo URLs + */ + public function buildEvent(array $e, string $base, array $imageUrls, bool $hasOrganization): ?array + { + $name = trim((string)($e['title'] ?? '')); + $start = (int)($e['eventstart'] ?? 0); + if ($name === '' || $start === 0) { + return null; + } + + $event = [ + '@type' => 'ExhibitionEvent', + 'name' => $name, + 'startDate' => date('Y-m-d', $start), + ]; + + $end = (int)($e['eventend'] ?? 0); + if ($end > 0) { + $event['endDate'] = date('Y-m-d', $end); + } + + $description = trim(strip_tags((string)($e['description'] ?? ''))); + if ($description === '') { + $description = trim((string)($e['teaser'] ?? '')); + } + if ($description !== '') { + $event['description'] = $description; + } + + // Location: Place with venue + city/country, or VirtualLocation for online. + $mode = (string)($e['attendancemode'] ?? 'offline'); + $venue = trim((string)($e['venue'] ?? '')); + $city = trim((string)($e['city'] ?? '')); + $country = trim((string)($e['country'] ?? '')); + + if ($mode === 'online') { + $url = trim((string)($e['eventurl'] ?? '')); + if ($url !== '') { + $event['location'] = [ + '@type' => 'VirtualLocation', + 'url' => $this->absUrl($url, $base), + ]; + } + } else { + $address = array_filter([ + '@type' => 'PostalAddress', + 'addressLocality' => $city !== '' ? $city : null, + 'addressCountry' => $country !== '' ? $country : null, + ]); + $place = array_filter([ + '@type' => 'Place', + 'name' => $venue !== '' ? $venue : ($city !== '' ? $city : null), + 'address' => count($address) > 1 ? $address : null, + ]); + if (count($place) > 1) { + $event['location'] = $place; + } + } + + $statusMap = [ + 'scheduled' => 'https://schema.org/EventScheduled', + 'cancelled' => 'https://schema.org/EventCancelled', + 'postponed' => 'https://schema.org/EventPostponed', + 'rescheduled' => 'https://schema.org/EventRescheduled', + 'movedonline' => 'https://schema.org/EventMovedOnline', + ]; + $event['eventStatus'] = $statusMap[(string)($e['eventstatus'] ?? 'scheduled')] ?? $statusMap['scheduled']; + + $modeMap = [ + 'offline' => 'https://schema.org/OfflineEventAttendanceMode', + 'online' => 'https://schema.org/OnlineEventAttendanceMode', + 'mixed' => 'https://schema.org/MixedEventAttendanceMode', + ]; + $event['eventAttendanceMode'] = $modeMap[$mode] ?? $modeMap['offline']; + + $images = []; + foreach ($imageUrls as $imageUrl) { + $abs = $this->absUrl((string)$imageUrl, $base); + if ($abs !== '') { + $images[] = $abs; + } + } + if ($images !== []) { + $event['image'] = $images; + } + + $eventUrl = trim((string)($e['eventurl'] ?? '')); + if ($eventUrl !== '' && !str_starts_with($eventUrl, 't3://')) { + $event['url'] = $this->absUrl($eventUrl, $base); + } + + if ($hasOrganization) { + $event['organizer'] = ['@id' => $base . '/#organization']; + } + + return $event; + } + /** * Wrap one or more schema nodes into a single @graph document string. * diff --git a/packages/vitec/Classes/Service/UsecaseSerializer.php b/packages/vitec/Classes/Service/UsecaseSerializer.php new file mode 100644 index 0000000..b7c793b --- /dev/null +++ b/packages/vitec/Classes/Service/UsecaseSerializer.php @@ -0,0 +1,530 @@ + lean card data for the list plugin + * serializeDetail() -> full detail (hero, content elements, related, seo) + * + * Inline content elements are resolved flat; the VITEC Columns content block + * (vitec_columns) is resolved specially into { header…, layout, columns:{…} }. + * + * All relation/FAL resolution lives here so the List/Show UserFunc renderers + * stay thin. Every resolver is exception-safe and returns null/[] on failure. + */ +final class UsecaseSerializer +{ + private const TABLE = 'tx_vitec_domain_model_usecase'; + private const IMAGE_WIDTHS = [400, 800, 1200, 1600]; + private const COLUMNS_CTYPE = 'vitec_columns'; + + /** + * @param array $u + * @return array + */ + public function serializeListItem(array $u): array + { + $uid = (int)$u['uid']; + + return [ + 'uid' => $uid, + 'title' => (string)($u['title'] ?? ''), + 'slug' => (string)($u['slug'] ?? ''), + 'subtitle' => (string)($u['subtitle'] ?? ''), + 'teaser' => (string)($u['teaser'] ?? ''), + 'cardImage' => $this->image($uid, 'card_image'), + 'customerLogo' => $this->image($uid, 'customer_logo'), + 'market' => $this->market((int)($u['market'] ?? 0)), + 'categories' => $this->categories($uid), + 'featured' => (bool)($u['featured'] ?? false), + 'layoutVariant' => (string)($u['layout_variant'] ?? 'standard'), + 'backgroundVariant' => (string)($u['background_variant'] ?? 'none'), + ]; + } + + /** + * @param array $u + * @return array + */ + public function serializeDetail(array $u): array + { + $uid = (int)$u['uid']; + $base = $this->serializeListItem($u); + + return array_merge($base, [ + 'hero' => [ + 'bgImage' => $this->image($uid, 'hero_bgimage'), + 'smallImage' => $this->image($uid, 'hero_small_image'), + 'video' => $this->video($uid, 'hero_video'), + 'overlayColor' => (string)($u['hero_overlay_color'] ?? ''), + 'overlayOpacity' => (float)($u['hero_overlay_opacity'] ?? 0), + 'layout' => (string)($u['hero_layout'] ?? 'fullscreen'), + 'textTheme' => (string)($u['text_theme'] ?? 'light'), + ], + 'contentElements' => $this->contentElements($uid), + 'related' => [ + 'show' => (bool)($u['show_related'] ?? true), + 'market' => $base['market'], + 'solutions' => $this->relationMulti('tx_vitec_usecase_solution_mm', 'tx_vitec_domain_model_solution', $uid), + 'products' => $this->relationMulti('tx_vitec_usecase_product_mm', 'tx_vitec_domain_model_product', $uid), + 'categories' => $base['categories'], + ], + 'seo' => $this->seo($u), + 'appearance' => [ + 'layoutVariant' => (string)($u['layout_variant'] ?? 'standard'), + 'backgroundVariant' => (string)($u['background_variant'] ?? 'none'), + 'accentColor' => (string)($u['accent_color'] ?? ''), + 'featured' => (bool)($u['featured'] ?? false), + 'heroLayout' => (string)($u['hero_layout'] ?? 'fullscreen'), + 'textTheme' => (string)($u['text_theme'] ?? 'light'), + ], + ]); + } + + // ---------------------------------------------------------------- images + + /** + * FAL image (with srcset). $table defaults to the usecase table but can be + * a content-block collection table (for Columns items). + * + * @return array|null + */ + public function image(int $recordUid, string $fieldName, ?string $table = null): ?array + { + try { + $table = $table ?? self::TABLE; + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $ref = $qb + ->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop') + ->from('sys_file_reference', 'sfr') + ->where( + $qb->expr()->eq('sfr.tablenames', $qb->createNamedParameter($table, ParameterType::STRING)), + $qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter($fieldName, ParameterType::STRING)), + $qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($recordUid, ParameterType::INTEGER)), + $qb->expr()->eq('sfr.deleted', 0), + $qb->expr()->eq('sfr.hidden', 0) + ) + ->orderBy('sfr.sorting_foreign') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$ref) { + return null; + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $imageService = GeneralUtility::makeInstance(ImageService::class); + $fileReference = $resourceFactory->getFileReferenceObject((int)$ref['uid']); + + $srcset = []; + foreach (self::IMAGE_WIDTHS as $width) { + $variant = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => $width, 'crop' => $ref['crop'] ?? null] + ); + $srcset[] = [ + 'url' => $imageService->getImageUri($variant), + 'width' => $width, + 'descriptor' => $width . 'w', + ]; + } + + $default = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => 800, 'crop' => $ref['crop'] ?? null] + ); + + return [ + 'uid' => (int)$ref['uid'], + 'url' => $imageService->getImageUri($default), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'description' => (string)($ref['description'] ?? ''), + 'srcset' => $srcset, + 'properties' => [ + 'width' => $fileReference->getProperty('width'), + 'height' => $fileReference->getProperty('height'), + 'mimeType' => $fileReference->getProperty('mime_type'), + ], + ]; + } catch (\Throwable $e) { + return null; + } + } + + /** + * FAL video (public URL only). $table as in image(). + * + * @return array|null + */ + public function video(int $recordUid, string $fieldName, ?string $table = null): ?array + { + try { + $table = $table ?? self::TABLE; + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $ref = $qb + ->select('uid', 'title', 'description') + ->from('sys_file_reference') + ->where( + $qb->expr()->eq('tablenames', $qb->createNamedParameter($table, ParameterType::STRING)), + $qb->expr()->eq('fieldname', $qb->createNamedParameter($fieldName, ParameterType::STRING)), + $qb->expr()->eq('uid_foreign', $qb->createNamedParameter($recordUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting_foreign') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$ref) { + return null; + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $fileReference = $resourceFactory->getFileReferenceObject((int)$ref['uid']); + + return [ + 'uid' => (int)$ref['uid'], + 'url' => $fileReference->getPublicUrl(), + 'title' => (string)($ref['title'] ?? ''), + 'mimeType' => $fileReference->getProperty('mime_type'), + ]; + } catch (\Throwable $e) { + return null; + } + } + + // ------------------------------------------------------------- relations + + /** + * Single market (foreign_table, no MM) -> title/slug. + * + * @return array|null + */ + public function market(int $marketUid): ?array + { + if ($marketUid <= 0) { + return null; + } + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_market'); + $row = $qb + ->select('uid', 'title', 'subtitle', 'teaser') + ->from('tx_vitec_domain_model_market') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($marketUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return [ + 'uid' => (int)$row['uid'], + 'title' => (string)($row['title'] ?? ''), + 'subtitle' => (string)($row['subtitle'] ?? ''), + 'teaser' => (string)($row['teaser'] ?? ''), + ]; + } catch (\Throwable $e) { + return null; + } + } + + /** + * n:n relation resolved through an MM table -> [{uid,title}, …]. + * + * @return array> + */ + public function relationMulti(string $mmTable, string $foreignTable, int $usecaseUid): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($foreignTable); + $rows = $qb + ->select('f.uid', 'f.title') + ->from($foreignTable, 'f') + ->join('f', $mmTable, 'mm', 'mm.uid_foreign = f.uid') + ->where( + $qb->expr()->eq('mm.uid_local', $qb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $qb->expr()->eq('f.deleted', 0), + $qb->expr()->eq('f.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + return array_map(static fn ($r) => [ + 'uid' => (int)$r['uid'], + 'title' => (string)($r['title'] ?? ''), + ], $rows); + } catch (\Throwable $e) { + return []; + } + } + + /** + * System categories (sys_category MM) -> [{uid,title,description}, …]. + * + * @return array> + */ + public function categories(int $usecaseUid): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + $rows = $qb + ->select('c.uid', 'c.title', 'c.description') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter(self::TABLE, ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + return array_map(static fn ($c) => [ + 'uid' => (int)$c['uid'], + 'title' => (string)($c['title'] ?? ''), + 'description' => (string)($c['description'] ?? ''), + ], $rows); + } catch (\Throwable $e) { + return []; + } + } + + // ------------------------------------------------------- content elements + + /** + * Inline content elements (tt_content owned via tx_vitec_usecase_content). + * Flat CEs are normalised; a VITEC Columns element is resolved specially. + * + * @return array> + */ + public function contentElements(int $usecaseUid): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $rows = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('tx_vitec_usecase_content', $qb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $out = []; + foreach ($rows as $row) { + $resolved = $this->resolveInlineElement($row); + if ($resolved !== null) { + $out[] = $resolved; + } + } + return $out; + } catch (\Throwable $e) { + return []; + } + } + + /** + * @param array $row + * @return array|null + */ + private function resolveInlineElement(array $row): ?array + { + if ((string)($row['CType'] ?? '') === self::COLUMNS_CTYPE) { + return $this->resolveColumnsElement($row); + } + return ContentElementResolver::normaliseRecord($row); + } + + /** + * VITEC Columns content block -> { header…, layout, columns:{left,right} }. + * Items live in a content-blocks collection table (discovered from TCA) and + * carry a `column` (left/right) plus a typed payload. + * + * @param array $row + * @return array + */ + private function resolveColumnsElement(array $row): array + { + $uid = (int)$row['uid']; + + $left = []; + $right = []; + + $table = $this->collectionTable('vitec_items') ?? $this->collectionTable('items'); + if ($table !== null) { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $items = $qb + ->select('*') + ->from($table) + ->where( + $qb->expr()->eq('foreign_table_parent_uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($items as $it) { + $column = (string)($it['vitec_column'] ?? $it['column'] ?? 'left'); + $resolved = $this->resolveColumnItem($it, $table); + if ($column === 'right') { + $right[] = $resolved; + } else { + $left[] = $resolved; + } + } + } catch (\Throwable $e) { + // collection table not ready / query failed -> empty columns + } + } + + return [ + 'id' => $uid, + 'type' => self::COLUMNS_CTYPE, + 'colPos' => (int)($row['colPos'] ?? 0), + 'sorting' => (int)($row['sorting'] ?? 0), + 'header' => (string)($row['header'] ?? ''), + 'subheader' => (string)($row['subheader'] ?? ''), + 'headerLayout' => (int)($row['header_layout'] ?? 0), + 'headerPosition' => (string)($row['header_position'] ?? ''), + 'headerLink' => (string)($row['header_link'] ?? ''), + 'layout' => (string)($row['vitec_layout'] ?? $row['layout'] ?? 'cols_50_50'), + 'columns' => [ + 'left' => $left, + 'right' => $right, + ], + ]; + } + + /** + * @param array $it + * @return array + */ + private function resolveColumnItem(array $it, string $table): array + { + $uid = (int)$it['uid']; + $get = static fn (string $k): string => (string)($it['vitec_' . $k] ?? $it[$k] ?? ''); + + $imageField = array_key_exists('vitec_image', $it) ? 'vitec_image' : 'image'; + $videoField = array_key_exists('vitec_video', $it) ? 'vitec_video' : 'video'; + + return [ + 'type' => $get('item_type') !== '' ? $get('item_type') : 'text', + 'headline' => $get('headline'), + 'text' => $get('text'), + 'image' => $this->image($uid, $imageField, $table), + 'video' => $this->video($uid, $videoField, $table), + 'link' => $get('link'), + 'linkLabel' => $get('link_label'), + ]; + } + + /** + * Resolve a content-blocks collection field's storage table from TCA. + */ + private function collectionTable(string $field): ?string + { + $t = $GLOBALS['TCA']['tt_content']['columns'][$field]['config']['foreign_table'] ?? null; + return (is_string($t) && $t !== '') ? $t : null; + } + + // ------------------------------------------------------------------- seo + + /** + * SEO bundle with fallback resolution (title/teaser/card/hero images). + * + * @param array $u + * @return array + */ + public function seo(array $u): array + { + $uid = (int)$u['uid']; + + $title = $this->firstNonEmpty([(string)($u['seo_title'] ?? ''), (string)($u['title'] ?? '')]); + $desc = $this->firstNonEmpty([(string)($u['seo_description'] ?? ''), (string)($u['teaser'] ?? '')]); + + $ogTitle = $this->firstNonEmpty([(string)($u['og_title'] ?? ''), $title]); + $ogDesc = $this->firstNonEmpty([(string)($u['og_description'] ?? ''), $desc]); + $ogImage = $this->image($uid, 'og_image') + ?? $this->image($uid, 'card_image') + ?? $this->image($uid, 'hero_bgimage'); + + $twTitle = $this->firstNonEmpty([(string)($u['twitter_title'] ?? ''), $ogTitle]); + $twDesc = $this->firstNonEmpty([(string)($u['twitter_description'] ?? ''), $ogDesc]); + $twImage = $this->image($uid, 'twitter_image') ?? $ogImage; + + return [ + 'title' => $title, + 'description' => $desc, + 'canonical' => (string)($u['canonical_link'] ?? ''), + 'robots' => [ + 'noIndex' => (bool)($u['no_index'] ?? false), + 'noFollow' => (bool)($u['no_follow'] ?? false), + ], + 'openGraph' => [ + 'title' => $ogTitle, + 'description' => $ogDesc, + 'image' => $ogImage, + ], + 'twitter' => [ + 'title' => $twTitle, + 'description' => $twDesc, + 'image' => $twImage, + ], + ]; + } + + /** + * @param array $candidates + */ + private function firstNonEmpty(array $candidates): string + { + foreach ($candidates as $c) { + $c = trim($c); + if ($c !== '') { + return $c; + } + } + return ''; + } +} diff --git a/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php b/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php old mode 100755 new mode 100644 index d82c17b..cbd3a75 --- a/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php @@ -7,6 +7,7 @@ namespace Evomedien\Vitec\UserFunc; use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Service\FlexFormService; @@ -49,13 +50,7 @@ class DatasheetsJsonRenderer ->from('tt_content') ->where( $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), - $qb->expr()->or( - $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_datasheets', ParameterType::STRING)), - $qb->expr()->and( - $qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)), - $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_datasheets', ParameterType::STRING)) - ) - ), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_datasheets', ParameterType::STRING)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) @@ -118,6 +113,7 @@ class DatasheetsJsonRenderer $response['debug'] = [ 'productCount' => count($itemsOut), 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromItems($itemsOut), ]; } @@ -259,6 +255,7 @@ class DatasheetsJsonRenderer private function serializeDatasheet(array $download): array { $uid = (int)$download['uid']; + $filetype = $this->getDownloadFileType($uid); return [ 'uid' => $uid, 'title' => (string)($download['title'] ?? ''), @@ -266,10 +263,48 @@ class DatasheetsJsonRenderer 'teaser' => (string)($download['teaser'] ?? ''), 'description' => (string)($download['description'] ?? ''), 'tstamp' => (int)($download['tstamp'] ?? 0), - 'file' => $this->getDownloadFile($uid), + 'filetype' => $filetype, + 'file' => $this->getDownloadFile($uid, (string)($download['fileprefix'] ?? ''), $filetype), ]; } + private function getDownloadFileType(int $downloadUid): string + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.filetype') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($categories as $category) { + $filetype = trim((string)($category['filetype'] ?? '')); + if ($filetype !== '') { + return $filetype; + } + } + + return ''; + } + /** * @return array|null */ @@ -356,7 +391,7 @@ class DatasheetsJsonRenderer /** * @return array|null */ - private function getDownloadFile(int $downloadUid): ?array + private function getDownloadFile(int $downloadUid, string $fileprefix = '', string $filetype = ''): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); @@ -378,7 +413,7 @@ class DatasheetsJsonRenderer ->fetchAssociative(); if (!$row) { - return null; + return $this->resolveFileByConvention($fileprefix, $filetype); } return [ @@ -392,4 +427,129 @@ class DatasheetsJsonRenderer 'description' => (string)($row['description'] ?? ''), ]; } + + /** + * Resolve file from naming convention: + * [fileprefix]__[filetype]__[number]-[letter].ext + * and pick highest number + highest letter. + * + * @return array|null + */ + private function resolveFileByConvention(string $fileprefix, string $filetype): ?array + { + $fileprefix = trim($fileprefix); + $filetype = trim($filetype); + if ($fileprefix === '' || $filetype === '') { + return null; + } + + $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; + if (!is_dir($baseDir) || !is_readable($baseDir)) { + return null; + } + + $escapedPrefix = preg_quote($fileprefix, '/'); + $escapedType = preg_quote($filetype, '/'); + $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; + + $bestFile = null; + $bestNumber = -1; + $bestLetterRank = -1; + + $entries = scandir($baseDir); + if ($entries === false) { + return null; + } + + foreach ($entries as $entry) { + if (!is_string($entry) || $entry === '.' || $entry === '..') { + continue; + } + + if (!preg_match($pattern, $entry, $matches)) { + continue; + } + + $number = (int)$matches[1]; + $letter = strtoupper($matches[2]); + $letterRank = $this->letterSequenceToRank($letter); + + if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { + $bestNumber = $number; + $bestLetterRank = $letterRank; + $bestFile = $entry; + } + } + + if ($bestFile === null) { + return null; + } + + $fullPath = $baseDir . '/' . $bestFile; + if (!is_file($fullPath) || !is_readable($fullPath)) { + return null; + } + + $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); + $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; + + return [ + 'uid' => 0, + 'name' => $bestFile, + 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), + 'size' => (int)(filesize($fullPath) ?: 0), + 'extension' => $extension, + 'mimeType' => $mimeType, + 'title' => '', + 'description' => '', + ]; + } + + private function letterSequenceToRank(string $letters): int + { + $rank = 0; + $letters = strtoupper($letters); + $length = strlen($letters); + + for ($i = 0; $i < $length; $i++) { + $char = ord($letters[$i]); + if ($char < 65 || $char > 90) { + continue; + } + + $rank = ($rank * 26) + ($char - 64); + } + + return $rank; + } + + /** + * @param array> $items + * @return array> + */ + private function collectFallbackFilesFromItems(array $items): array + { + $fallbackFiles = []; + + foreach ($items as $item) { + $datasheet = $item['datasheet'] ?? null; + if (!is_array($datasheet)) { + continue; + } + + $file = $datasheet['file'] ?? null; + if (!is_array($file) || (int)($file['uid'] ?? 1) !== 0) { + continue; + } + + $fallbackFiles[] = [ + 'datasheetUid' => (int)($datasheet['uid'] ?? 0), + 'name' => (string)($file['name'] ?? ''), + 'url' => (string)($file['url'] ?? ''), + 'source' => 'naming-convention-fallback', + ]; + } + + return $fallbackFiles; + } } diff --git a/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php b/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php old mode 100755 new mode 100644 index eff7c91..a24ba70 --- a/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php @@ -7,6 +7,7 @@ namespace Evomedien\Vitec\UserFunc; use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -44,20 +45,12 @@ class DownloadcardJsonRenderer $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); - // After v14 migration the CType is the plugin signature; for legacy - // (unmigrated) records the CType is still "list" with list_type set. $rows = $qb ->select('*') ->from('tt_content') ->where( $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), - $qb->expr()->or( - $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING)), - $qb->expr()->and( - $qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)), - $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING)) - ) - ), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) @@ -106,14 +99,17 @@ class DownloadcardJsonRenderer : ''; } + $serializedDownload = $this->serializeDownload($download); + $response = [ 'mode' => 'single', - 'download' => $this->serializeDownload($download), + 'download' => $serializedDownload, 'settings' => $settingsOut, ]; if ($debugMode) { $response['debug'] = [ 'pageId' => $pageId, 'downloadUid' => $downloadUid, 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromDownloads([$serializedDownload]), ]; } return json_encode($response); @@ -123,11 +119,12 @@ class DownloadcardJsonRenderer if ($productUid > 0) { $downloads = $this->fetchDownloadsForProduct($productUid); $product = $this->fetchProductContext($productUid); + $serializedDownloads = array_map(fn($r) => $this->serializeDownload($r), $downloads); $response = [ 'mode' => 'product-downloads', 'product' => $product, - 'downloads' => array_map(fn($r) => $this->serializeDownload($r), $downloads), + 'downloads' => $serializedDownloads, 'settings' => $settingsOut, ]; if ($debugMode) { @@ -136,6 +133,7 @@ class DownloadcardJsonRenderer 'productUid' => $productUid, 'downloadCount' => count($downloads), 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromDownloads($serializedDownloads), ]; } return json_encode($response); @@ -248,6 +246,8 @@ class DownloadcardJsonRenderer protected function serializeDownload(array $download): array { $uid = (int)$download['uid']; + $fileprefix = (string)($download['fileprefix'] ?? ''); + $filetype = $this->getDownloadFileType($uid); return [ 'uid' => $uid, @@ -258,22 +258,60 @@ class DownloadcardJsonRenderer 'keywords' => (string)($download['keywords'] ?? ''), 'icon' => (string)($download['icon'] ?? ''), 'filepath' => (string)($download['filepath'] ?? ''), - 'fileprefix' => (string)($download['fileprefix'] ?? ''), + 'fileprefix' => $fileprefix, + 'filetype' => $filetype, 'private_download' => (bool)($download['private_download'] ?? false), 'hideonapp' => (bool)($download['hideonapp'] ?? false), 'hideonwebsite' => (bool)($download['hideonwebsite'] ?? false), 'hideondatasheets' => (bool)($download['hideondatasheets'] ?? false), 'hideonproducts' => (bool)($download['hideonproducts'] ?? false), - 'file' => $this->getDownloadFile($uid), + 'file' => $this->getDownloadFile($uid, $fileprefix, $filetype), ]; } + private function getDownloadFileType(int $downloadUid): string + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.filetype') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($categories as $category) { + $filetype = trim((string)($category['filetype'] ?? '')); + if ($filetype !== '') { + return $filetype; + } + } + + return ''; + } + /** * Resolve the FAL file reference (fieldname=file) for a download. * * @return array|null */ - protected function getDownloadFile(int $downloadUid): ?array + protected function getDownloadFile(int $downloadUid, string $fileprefix = '', string $filetype = ''): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); @@ -295,7 +333,7 @@ class DownloadcardJsonRenderer ->fetchAssociative(); if (!$row) { - return null; + return $this->resolveFileByConvention($fileprefix, $filetype); } $thumbnailUrl = null; @@ -324,4 +362,124 @@ class DownloadcardJsonRenderer 'description' => (string)($row['description'] ?? ''), ]; } + + /** + * @return array|null + */ + private function resolveFileByConvention(string $fileprefix, string $filetype): ?array + { + $fileprefix = trim($fileprefix); + $filetype = trim($filetype); + if ($fileprefix === '' || $filetype === '') { + return null; + } + + $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; + if (!is_dir($baseDir) || !is_readable($baseDir)) { + return null; + } + + $escapedPrefix = preg_quote($fileprefix, '/'); + $escapedType = preg_quote($filetype, '/'); + $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; + + $bestFile = null; + $bestNumber = -1; + $bestLetterRank = -1; + + $entries = scandir($baseDir); + if ($entries === false) { + return null; + } + + foreach ($entries as $entry) { + if (!is_string($entry) || $entry === '.' || $entry === '..') { + continue; + } + + if (!preg_match($pattern, $entry, $matches)) { + continue; + } + + $number = (int)$matches[1]; + $letterRank = $this->letterSequenceToRank((string)$matches[2]); + + if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { + $bestNumber = $number; + $bestLetterRank = $letterRank; + $bestFile = $entry; + } + } + + if ($bestFile === null) { + return null; + } + + $fullPath = $baseDir . '/' . $bestFile; + if (!is_file($fullPath) || !is_readable($fullPath)) { + return null; + } + + $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); + $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; + + return [ + 'uid' => 0, + 'name' => $bestFile, + 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), + 'thumbnail' => null, + 'size' => (int)(filesize($fullPath) ?: 0), + 'extension' => $extension, + 'mimeType' => $mimeType, + 'title' => '', + 'description' => '', + ]; + } + + private function letterSequenceToRank(string $letters): int + { + $rank = 0; + $letters = strtoupper($letters); + $length = strlen($letters); + + for ($i = 0; $i < $length; $i++) { + $char = ord($letters[$i]); + if ($char < 65 || $char > 90) { + continue; + } + + $rank = ($rank * 26) + ($char - 64); + } + + return $rank; + } + + /** + * @param array> $downloads + * @return array> + */ + private function collectFallbackFilesFromDownloads(array $downloads): array + { + $fallbackFiles = []; + + foreach ($downloads as $download) { + $file = $download['file'] ?? null; + if (!is_array($file)) { + continue; + } + + if ((int)($file['uid'] ?? 1) !== 0) { + continue; + } + + $fallbackFiles[] = [ + 'downloadUid' => (int)($download['uid'] ?? 0), + 'name' => (string)($file['name'] ?? ''), + 'url' => (string)($file['url'] ?? ''), + 'source' => 'naming-convention-fallback', + ]; + } + + return $fallbackFiles; + } } diff --git a/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php b/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php old mode 100755 new mode 100644 index e742ff5..64219da --- a/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php @@ -7,6 +7,7 @@ namespace Evomedien\Vitec\UserFunc; use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -96,13 +97,14 @@ class DownloadcardcollectionJsonRenderer // --- Downloads: comma-separated UID list from settings.download --- $downloadUids = $this->parseUidList($settings['download'] ?? ''); $downloads = $downloadUids === [] ? [] : $this->fetchDownloadsByUids($downloadUids); + $serializedDownloads = array_map(fn($r) => $this->serializeDownload($r), $downloads); // --- Header image (inline FAL on this tt_content element, fieldname=image) --- $image = $this->getCollectionImage($ttContentUid); $response = [ 'image' => $image, - 'downloads' => array_map(fn($r) => $this->serializeDownload($r), $downloads), + 'downloads' => $serializedDownloads, 'settings' => $settingsOut, ]; @@ -112,6 +114,7 @@ class DownloadcardcollectionJsonRenderer 'downloadUids' => $downloadUids, 'downloadCount' => count($downloads), 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromDownloads($serializedDownloads), ]; } @@ -212,7 +215,6 @@ class DownloadcardcollectionJsonRenderer ); $srcset[] = [ 'url' => $imageService->getImageUri($variant), - 'thumbnail' => $thumbnailUrl, 'width' => $width, 'descriptor' => $width . 'w', ]; @@ -226,7 +228,6 @@ class DownloadcardcollectionJsonRenderer return [ 'uid' => (int)$fileRefData['uid'], 'url' => $imageService->getImageUri($default), - 'thumbnail' => $thumbnailUrl, 'title' => $fileRefData['title'] ?? '', 'alternative' => $fileRefData['alternative'] ?? '', 'description' => $fileRefData['description'] ?? '', @@ -249,6 +250,8 @@ class DownloadcardcollectionJsonRenderer protected function serializeDownload(array $download): array { $uid = (int)$download['uid']; + $fileprefix = (string)($download['fileprefix'] ?? ''); + $filetype = $this->getDownloadFileType($uid); return [ 'uid' => $uid, @@ -259,22 +262,60 @@ class DownloadcardcollectionJsonRenderer 'keywords' => (string)($download['keywords'] ?? ''), 'icon' => (string)($download['icon'] ?? ''), 'filepath' => (string)($download['filepath'] ?? ''), - 'fileprefix' => (string)($download['fileprefix'] ?? ''), + 'fileprefix' => $fileprefix, + 'filetype' => $filetype, 'private_download' => (bool)($download['private_download'] ?? false), 'hideonapp' => (bool)($download['hideonapp'] ?? false), 'hideonwebsite' => (bool)($download['hideonwebsite'] ?? false), 'hideondatasheets' => (bool)($download['hideondatasheets'] ?? false), 'hideonproducts' => (bool)($download['hideonproducts'] ?? false), - 'file' => $this->getDownloadFile($uid), + 'file' => $this->getDownloadFile($uid, $fileprefix, $filetype), ]; } + private function getDownloadFileType(int $downloadUid): string + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.filetype') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($categories as $category) { + $filetype = trim((string)($category['filetype'] ?? '')); + if ($filetype !== '') { + return $filetype; + } + } + + return ''; + } + /** * Resolve the FAL file reference (fieldname=file) for a download. * * @return array|null */ - protected function getDownloadFile(int $downloadUid): ?array + protected function getDownloadFile(int $downloadUid, string $fileprefix = '', string $filetype = ''): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); @@ -296,7 +337,7 @@ class DownloadcardcollectionJsonRenderer ->fetchAssociative(); if (!$row) { - return null; + return $this->resolveFileByConvention($fileprefix, $filetype); } @@ -325,4 +366,124 @@ class DownloadcardcollectionJsonRenderer 'description' => (string)($row['description'] ?? ''), ]; } + + /** + * @return array|null + */ + private function resolveFileByConvention(string $fileprefix, string $filetype): ?array + { + $fileprefix = trim($fileprefix); + $filetype = trim($filetype); + if ($fileprefix === '' || $filetype === '') { + return null; + } + + $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; + if (!is_dir($baseDir) || !is_readable($baseDir)) { + return null; + } + + $escapedPrefix = preg_quote($fileprefix, '/'); + $escapedType = preg_quote($filetype, '/'); + $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; + + $bestFile = null; + $bestNumber = -1; + $bestLetterRank = -1; + + $entries = scandir($baseDir); + if ($entries === false) { + return null; + } + + foreach ($entries as $entry) { + if (!is_string($entry) || $entry === '.' || $entry === '..') { + continue; + } + + if (!preg_match($pattern, $entry, $matches)) { + continue; + } + + $number = (int)$matches[1]; + $letterRank = $this->letterSequenceToRank((string)$matches[2]); + + if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { + $bestNumber = $number; + $bestLetterRank = $letterRank; + $bestFile = $entry; + } + } + + if ($bestFile === null) { + return null; + } + + $fullPath = $baseDir . '/' . $bestFile; + if (!is_file($fullPath) || !is_readable($fullPath)) { + return null; + } + + $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); + $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; + + return [ + 'uid' => 0, + 'name' => $bestFile, + 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), + 'thumbnail' => null, + 'size' => (int)(filesize($fullPath) ?: 0), + 'extension' => $extension, + 'mimeType' => $mimeType, + 'title' => '', + 'description' => '', + ]; + } + + private function letterSequenceToRank(string $letters): int + { + $rank = 0; + $letters = strtoupper($letters); + $length = strlen($letters); + + for ($i = 0; $i < $length; $i++) { + $char = ord($letters[$i]); + if ($char < 65 || $char > 90) { + continue; + } + + $rank = ($rank * 26) + ($char - 64); + } + + return $rank; + } + + /** + * @param array> $downloads + * @return array> + */ + private function collectFallbackFilesFromDownloads(array $downloads): array + { + $fallbackFiles = []; + + foreach ($downloads as $download) { + $file = $download['file'] ?? null; + if (!is_array($file)) { + continue; + } + + if ((int)($file['uid'] ?? 1) !== 0) { + continue; + } + + $fallbackFiles[] = [ + 'downloadUid' => (int)($download['uid'] ?? 0), + 'name' => (string)($file['name'] ?? ''), + 'url' => (string)($file['url'] ?? ''), + 'source' => 'naming-convention-fallback', + ]; + } + + return $fallbackFiles; + } } diff --git a/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php new file mode 100644 index 0000000..7fbe5ab --- /dev/null +++ b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php @@ -0,0 +1,314 @@ +data or page discovery). + * `renderForRecord()` = one specific tt_content row; reused by + * ContainerChildrenProcessor / ContentElementResolver. Exception-safe. + */ +class EventlistJsonRenderer +{ + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + // 1) cObj data path + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_eventlist') { + return $this->renderForRecord($row); + } + + // 2) Page discovery via v14 request attribute + $pageId = 0; + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $pageInfo = $request->getAttribute('frontend.page.information'); + if ($pageInfo !== null) { + $pageId = (int)$pageInfo->getId(); + } + } + if ($pageId <= 0) { + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + } + if ($pageId <= 0) { + return ''; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $contentElements = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_eventlist', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($contentElements)) { + return ''; + } + + return $this->renderForRecord($contentElements[0]); + } + + /** + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $showPast = (bool)($settings['showpast'] ?? false); + $daysInAdvance = (int)($settings['daysinadvance'] ?? 0); + $limit = (int)($settings['limit'] ?? 0); + $debugMode = (bool)($settings['debug'] ?? false); + + $upcoming = $this->fetchEvents(true, $limit, $daysInAdvance); + + $response = [ + 'events' => array_map(fn($e) => $this->serializeEvent($e), $upcoming), + 'settings' => [ + 'showpast' => $showPast, + 'daysinadvance' => $daysInAdvance, + 'limit' => $limit, + ], + ]; + + if ($showPast) { + $past = $this->fetchEvents(false, $limit); + $response['pastEvents'] = array_map(fn($e) => $this->serializeEvent($e), $past); + } + + if ($debugMode) { + $response['debug'] = [ + 'upcomingCount' => count($upcoming), + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @return array> + */ + private function fetchEvents(bool $upcoming, int $limit, int $daysInAdvance = 0): array + { + $todayMidnight = strtotime('today'); + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_event'); + $expr = $qb->expr(); + + $qb + ->select('*') + ->from('tx_vitec_domain_model_event') + ->where( + $expr->eq('deleted', 0), + $expr->eq('hidden', 0), + $expr->eq('hideonwebsite', 0) + ); + + if ($upcoming) { + // running or future: end >= today, or (no end and start >= today) + $qb->andWhere( + $expr->or( + $expr->gte('eventend', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER)), + $expr->and( + $expr->eq('eventend', 0), + $expr->gte('eventstart', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER)) + ) + ) + ); + $qb->orderBy('eventstart', 'ASC'); + if ($daysInAdvance > 0) { + $qb->andWhere( + $expr->lt('eventstart', $qb->createNamedParameter($todayMidnight + ($daysInAdvance * 86400), ParameterType::INTEGER)) + ); + } + } else { + // past: (end > 0 and end < today) or (no end and 0 < start < today) + $qb->andWhere( + $expr->or( + $expr->and( + $expr->gt('eventend', 0), + $expr->lt('eventend', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER)) + ), + $expr->and( + $expr->eq('eventend', 0), + $expr->gt('eventstart', 0), + $expr->lt('eventstart', $qb->createNamedParameter($todayMidnight, ParameterType::INTEGER)) + ) + ) + ); + $qb->orderBy('eventstart', 'DESC'); + } + + if ($limit > 0) { + $qb->setMaxResults($limit); + } + + return $qb->executeQuery()->fetchAllAssociative(); + } + + /** + * @param array $event + * @return array + */ + protected function serializeEvent(array $event): array + { + $uid = (int)$event['uid']; + $start = (int)($event['eventstart'] ?? 0); + $end = (int)($event['eventend'] ?? 0); + + return [ + 'uid' => $uid, + 'title' => (string)($event['title'] ?? ''), + 'slug' => (string)($event['slug'] ?? ''), + 'teaser' => (string)($event['teaser'] ?? ''), + 'description' => (string)($event['description'] ?? ''), + 'eventstart' => $start > 0 ? date('Y-m-d', $start) : null, + 'eventend' => $end > 0 ? date('Y-m-d', $end) : null, + 'venue' => (string)($event['venue'] ?? ''), + 'booth' => (string)($event['booth'] ?? ''), + 'city' => (string)($event['city'] ?? ''), + 'country' => (string)($event['country'] ?? ''), + 'attendancemode' => (string)($event['attendancemode'] ?? 'offline'), + 'eventstatus' => (string)($event['eventstatus'] ?? 'scheduled'), + 'eventurl' => (string)($event['eventurl'] ?? ''), + 'meetinglink' => (string)($event['meetinglink'] ?? ''), + 'image' => $this->getEventImage($uid), + 'categories' => $this->getEventCategories($uid), + ]; + } + + /** + * @return array|null + */ + protected function getEventImage(int $eventUid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + + $fileRefData = $qb + ->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop') + ->from('sys_file_reference', 'sfr') + ->where( + $qb->expr()->eq('sfr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_event', ParameterType::STRING)), + $qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter('image', ParameterType::STRING)), + $qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($eventUid, ParameterType::INTEGER)), + $qb->expr()->eq('sfr.deleted', 0), + $qb->expr()->eq('sfr.hidden', 0) + ) + ->orderBy('sfr.sorting_foreign') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$fileRefData) { + return null; + } + + try { + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $imageService = GeneralUtility::makeInstance(ImageService::class); + $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); + + $srcset = []; + foreach ([400, 800] as $width) { + $variant = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => $width, 'crop' => $fileRefData['crop'] ?? null] + ); + $srcset[] = [ + 'url' => $imageService->getImageUri($variant), + 'width' => $width, + 'descriptor' => $width . 'w', + ]; + } + + $default = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => 400, 'crop' => $fileRefData['crop'] ?? null] + ); + + return [ + 'uid' => (int)$fileRefData['uid'], + 'url' => $imageService->getImageUri($default), + 'title' => $fileRefData['title'] ?? '', + 'alternative' => $fileRefData['alternative'] ?? '', + 'description' => $fileRefData['description'] ?? '', + 'srcset' => $srcset, + 'properties' => [ + 'width' => $fileReference->getProperty('width'), + 'height' => $fileReference->getProperty('height'), + 'mimeType' => $fileReference->getProperty('mime_type'), + ], + ]; + } catch (\Exception $e) { + return null; + } + } + + protected function getEventCategories(int $eventUid): array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.uid', 'c.title', 'c.description') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_event', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($eventUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + return array_map(static function ($cat) { + return [ + 'uid' => (int)$cat['uid'], + 'title' => (string)($cat['title'] ?? ''), + 'description' => (string)($cat['description'] ?? ''), + ]; + }, $categories); + } +} diff --git a/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php new file mode 100644 index 0000000..8e09ee1 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php @@ -0,0 +1,753 @@ + 'Compact List', + '2' => 'Featured Story', + '3' => 'Editorial Grid', + '4' => 'Detail Spotlight', + ]; + + private const TEMPLATE_LAYOUT_DEFAULTS = [ + 'news_pi1' => '1', + 'news_newsliststicky' => '1', + 'news_newsselectedlist' => '1', + 'news_newsdetail' => '2', + 'news_newsdatemenu' => '3', + 'news_categorylist' => '3', + 'news_newssearchform' => '4', + 'news_newssearchresult' => '4', + 'news_taglist' => '3', + ]; + + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + try { + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row !== null && in_array((string)($row['CType'] ?? ''), self::NEWS_CTYPES, true)) { + return $this->renderForRecord($row); + } + + $pageId = $this->resolveCurrentPageId(); + if ($pageId <= 0) { + return ''; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $ctypeConstraints = []; + foreach (self::NEWS_CTYPES as $ctype) { + $ctypeConstraints[] = $queryBuilder->expr()->eq( + 'CType', + $queryBuilder->createNamedParameter($ctype, ParameterType::STRING) + ); + } + + $contentElements = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), + $queryBuilder->expr()->or(...$ctypeConstraints), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + if ($contentElements === []) { + return ''; + } + + return $this->renderForRecord($contentElements[0]); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * Render exactly the given news tt_content row. + * + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $type = (string)($contentElement['CType'] ?? ''); + $mode = $type === 'news_newsdetail' ? 'detail' : 'list'; + $debug = (bool)($settings['debug'] ?? false); + + if ($mode === 'detail') { + $news = $this->resolveDetailNews($contentElement, $settings); + $response = [ + 'mode' => 'detail', + 'news' => $news !== null ? $this->serializeNews($news, $settings) : null, + 'settings' => $this->buildSettingsPayload($contentElement, $settings), + ]; + + if ($debug) { + $response['debug'] = [ + 'contentElementUid' => (int)($contentElement['uid'] ?? 0), + 'settings' => $settings, + ]; + } + + return json_encode($response) ?: ''; + } + + $newsRows = $this->fetchNewsList($contentElement, $settings); + $items = array_map(fn (array $newsRow): array => $this->serializeNews($newsRow, $settings), $newsRows); + + $response = [ + 'mode' => 'list', + 'items' => $items, + 'settings' => $this->buildSettingsPayload($contentElement, $settings), + ]; + + if ($debug) { + $response['debug'] = [ + 'contentElementUid' => (int)($contentElement['uid'] ?? 0), + 'count' => count($items), + 'settings' => $settings, + ]; + } + + return json_encode($response) ?: ''; + } catch (\Throwable $e) { + return json_encode([ + 'mode' => 'error', + 'message' => $e->getMessage(), + ]) ?: ''; + } + } + + /** + * @param array $contentElement + * @param array $settings + * @return array|null + */ + private function resolveDetailNews(array $contentElement, array $settings): ?array + { + $newsUid = $this->resolveNewsUid((string)($settings['singleNews'] ?? '')); + if ($newsUid <= 0) { + $queryParams = $GLOBALS['TYPO3_REQUEST']?->getQueryParams() ?? []; + $newsParam = $queryParams['tx_news_pi1']['news'] ?? null; + $newsUid = $this->resolveNewsUid((string)($newsParam ?? '')); + } + + if ($newsUid > 0) { + $news = $this->fetchNewsByUid($newsUid, (int)($contentElement['sys_language_uid'] ?? 0)); + if ($news !== null) { + return $news; + } + } + + $list = $this->fetchNewsList($contentElement, $settings, 1); + return $list[0] ?? null; + } + + /** + * @param array $contentElement + * @param array $settings + * @param int|null $limitOverride + * @return array> + */ + private function fetchNewsList(array $contentElement, array $settings, ?int $limitOverride = null): array + { + $selectedNews = $this->parseUidList((string)($settings['selectedList'] ?? '')); + $storagePids = $this->parseUidList((string)($settings['startingpoint'] ?? '')); + if ($storagePids === []) { + $storagePids = [(int)($contentElement['pid'] ?? 0)]; + } + + $categoryUids = $this->parseUidList((string)($settings['categories'] ?? '')); + $includeSubCategories = (bool)($settings['includeSubCategories'] ?? false); + $tagUids = $this->parseUidList((string)($settings['tags'] ?? '')); + $limit = $limitOverride ?? (int)($settings['limit'] ?? 0); + $orderDirection = strtoupper((string)($settings['orderDirection'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; + $topNewsFirst = (bool)($settings['topNewsFirst'] ?? false); + $searchTerm = $this->resolveSearchTerm((string)($contentElement['CType'] ?? '')); + $languageUid = (int)($contentElement['sys_language_uid'] ?? 0); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_news'); + + $rows = $queryBuilder + ->select('n.*') + ->from('tx_news_domain_model_news', 'n') + ->where( + $queryBuilder->expr()->eq('n.deleted', 0), + $queryBuilder->expr()->eq('n.hidden', 0), + $queryBuilder->expr()->in('n.pid', $queryBuilder->createNamedParameter($storagePids, ArrayParameterType::INTEGER)) + ) + ->orderBy('n.istopnews', $topNewsFirst ? 'DESC' : 'ASC') + ->addOrderBy('n.datetime', 'DESC') + ->executeQuery() + ->fetchAllAssociative(); + + if ($languageUid > 0) { + $rows = array_values(array_filter($rows, static fn (array $row): bool => (int)($row['sys_language_uid'] ?? 0) === $languageUid)); + } else { + $rows = array_values(array_filter($rows, static fn (array $row): bool => in_array((int)($row['sys_language_uid'] ?? 0), [0, -1], true))); + } + + if ($selectedNews !== []) { + $selectedLookup = array_fill_keys($selectedNews, true); + $rows = array_values(array_filter($rows, static fn (array $row): bool => isset($selectedLookup[(int)($row['uid'] ?? 0)]))); + } + + if ($categoryUids !== []) { + $categoryLookup = $this->newsUidsForCategories($categoryUids, $includeSubCategories); + if ($categoryLookup === [] && !$includeSubCategories) { + $categoryLookup = $this->newsUidsForCategories($this->expandCategoryUids($categoryUids), true); + } + + if ($categoryLookup !== []) { + $rows = array_values(array_filter($rows, static fn (array $row): bool => isset($categoryLookup[(int)($row['uid'] ?? 0)]))); + } + } + + if ($tagUids !== []) { + $tagLookup = $this->newsUidsForTags($tagUids); + if ($tagLookup !== []) { + $rows = array_values(array_filter($rows, static fn (array $row): bool => isset($tagLookup[(int)($row['uid'] ?? 0)]))); + } + } + + if ($searchTerm !== '') { + $needle = mb_strtolower($searchTerm); + $rows = array_values(array_filter($rows, static function (array $row) use ($needle): bool { + $haystack = mb_strtolower((string)($row['title'] ?? '') . ' ' . (string)($row['teaser'] ?? '') . ' ' . (string)($row['path_segment'] ?? '')); + return str_contains($haystack, $needle); + })); + } + + if ($limit > 0) { + $rows = array_slice($rows, 0, $limit); + } + + return $rows; + } + + /** + * @param int $newsUid + * @param int $languageUid + * @return array|null + */ + private function fetchNewsByUid(int $newsUid, int $languageUid): ?array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_news'); + + $query = $queryBuilder + ->select('n.*') + ->from('tx_news_domain_model_news', 'n') + ->where( + $queryBuilder->expr()->eq('n.uid', $queryBuilder->createNamedParameter($newsUid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('n.deleted', 0), + $queryBuilder->expr()->eq('n.hidden', 0) + ); + + if ($languageUid > 0) { + $query->andWhere( + $queryBuilder->expr()->eq('n.sys_language_uid', $queryBuilder->createNamedParameter($languageUid, ParameterType::INTEGER)) + ); + } + + $news = $query->executeQuery()->fetchAssociative(); + return is_array($news) ? $news : null; + } + + /** + * @param array $news + * @param array $settings + * @return array + */ + private function serializeNews(array $news, array $settings = []): array + { + $uid = (int)($news['uid'] ?? 0); + $pathSegment = (string)($news['path_segment'] ?? ''); + $urls = $this->buildNewsUrls($pathSegment, $settings); + + return [ + 'uid' => $uid, + 'title' => (string)($news['title'] ?? ''), + 'alternativeTitle' => (string)($news['alternative_title'] ?? ''), + 'pathSegment' => $pathSegment, + 'detailUrl' => $urls['detailUrl'], + 'canonicalUrl' => $urls['canonicalUrl'], + 'teaser' => (string)($news['teaser'] ?? ''), + 'bodytext' => (string)($news['bodytext'] ?? ''), + 'datetime' => (int)($news['datetime'] ?? 0), + 'archive' => (int)($news['archive'] ?? 0), + 'istopnews' => (bool)($news['istopnews'] ?? false), + 'author' => (string)($news['author'] ?? ''), + 'authorEmail' => (string)($news['author_email'] ?? ''), + 'categories' => $this->resolveCategories($uid), + 'tags' => $this->resolveTags($uid), + 'media' => $this->resolveFiles('tx_news_domain_model_news', 'fal_media', $uid), + 'relatedFiles' => $this->resolveFiles('tx_news_domain_model_news', 'fal_related_files', $uid), + 'related' => $this->resolveRelatedNews($uid), + 'relatedLinks' => $this->resolveRelatedLinks($uid), + ]; + } + + /** + * @param array $settings + * @return array{detailUrl:string, canonicalUrl:string} + */ + private function buildNewsUrls(string $pathSegment, array $settings): array + { + $pathSegment = trim($pathSegment, '/'); + if ($pathSegment === '') { + return ['detailUrl' => '', 'canonicalUrl' => '']; + } + + $detailPid = (int)($settings['detailPid'] ?? 0); + if ($detailPid <= 0) { + $detailPid = $this->resolveCurrentPageId(); + } + $detailPageSlug = $detailPid > 0 ? $this->resolvePageSlug($detailPid) : ''; + if ($detailPageSlug === '') { + return ['detailUrl' => '', 'canonicalUrl' => '']; + } + + $detailUrl = rtrim($detailPageSlug, '/') . '/' . $pathSegment; + $canonicalUrl = $this->absoluteUrl($detailUrl); + + return [ + 'detailUrl' => $detailUrl, + 'canonicalUrl' => $canonicalUrl, + ]; + } + + private function resolvePageSlug(int $pageId): string + { + if ($pageId <= 0) { + return ''; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('pages'); + + $row = $queryBuilder + ->select('slug') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + return (string)($row['slug'] ?? ''); + } + + private function absoluteUrl(string $path): string + { + $path = '/' . ltrim($path, '/'); + + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $normalizedParams = $request->getAttribute('normalizedParams'); + if ($normalizedParams !== null) { + return rtrim($normalizedParams->getSiteUrl(), '/') . $path; + } + } + + $site = $request?->getAttribute('site'); + if ($site !== null) { + return rtrim((string)$site->getBase(), '/') . $path; + } + + return $path; + } + + /** + * @return array> + */ + private function resolveCategories(int $newsUid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + return $queryBuilder + ->select('c.uid', 'c.title') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid' + ) + ->where( + $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($newsUid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', ParameterType::STRING)), + $queryBuilder->expr()->eq('mm.fieldname', $queryBuilder->createNamedParameter('categories', ParameterType::STRING)) + ) + ->orderBy('c.title', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * @return array> + */ + private function resolveTags(int $newsUid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_tag'); + + return $queryBuilder + ->select('t.uid', 't.title') + ->from('tx_news_domain_model_tag', 't') + ->join( + 't', + 'tx_news_domain_model_news_tag_mm', + 'mm', + 'mm.uid_foreign = t.uid' + ) + ->where( + $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($newsUid, ParameterType::INTEGER)) + ) + ->orderBy('t.title', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * @param list $categoryUids + * @return array + */ + private function newsUidsForCategories(array $categoryUids, bool $includeSubCategories): array + { + if ($categoryUids === []) { + return []; + } + + $categories = $includeSubCategories ? $this->expandCategoryUids($categoryUids) : $categoryUids; + if ($categories === []) { + return []; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category_record_mm'); + + $uids = $queryBuilder + ->select('uid_foreign') + ->from('sys_category_record_mm') + ->where( + $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', ParameterType::STRING)), + $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('categories', ParameterType::STRING)), + $queryBuilder->expr()->in('uid_local', $queryBuilder->createNamedParameter($categories, ArrayParameterType::INTEGER)) + ) + ->executeQuery() + ->fetchFirstColumn(); + + return array_fill_keys(array_map('intval', $uids), true); + } + + /** + * @param list $tagUids + * @return array + */ + private function newsUidsForTags(array $tagUids): array + { + if ($tagUids === []) { + return []; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_news_tag_mm'); + + $uids = $queryBuilder + ->select('uid_local') + ->from('tx_news_domain_model_news_tag_mm') + ->where( + $queryBuilder->expr()->in('uid_foreign', $queryBuilder->createNamedParameter($tagUids, ArrayParameterType::INTEGER)) + ) + ->executeQuery() + ->fetchFirstColumn(); + + return array_fill_keys(array_map('intval', $uids), true); + } + + /** + * @return array> + */ + private function resolveRelatedNews(int $newsUid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_news'); + + return $queryBuilder + ->select('n.uid', 'n.title', 'n.path_segment') + ->from('tx_news_domain_model_news', 'n') + ->join( + 'n', + 'tx_news_domain_model_news_related_mm', + 'mm', + 'mm.uid_foreign = n.uid' + ) + ->where( + $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($newsUid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('n.deleted', 0), + $queryBuilder->expr()->eq('n.hidden', 0) + ) + ->orderBy('n.datetime', 'DESC') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * @return array> + */ + private function resolveRelatedLinks(int $newsUid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_link'); + + return $queryBuilder + ->select('l.uid', 'l.title', 'l.description', 'l.uri') + ->from('tx_news_domain_model_link', 'l') + ->where( + $queryBuilder->expr()->eq('l.parent', $queryBuilder->createNamedParameter($newsUid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('l.deleted', 0), + $queryBuilder->expr()->eq('l.hidden', 0) + ) + ->orderBy('l.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * @return array> + */ + private function resolveFiles(string $tableName, string $fieldName, int $uid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + + $rows = $queryBuilder + ->select('uid') + ->from('sys_file_reference') + ->where( + $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($uid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter($tableName, ParameterType::STRING)), + $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('hidden', 0) + ) + ->orderBy('sorting_foreign', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + if ($rows === []) { + return []; + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $imageService = GeneralUtility::makeInstance(ImageService::class); + $files = []; + + foreach ($rows as $row) { + try { + $fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']); + $processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400]); + + $files[] = [ + 'uid' => (int)$row['uid'], + 'url' => $imageService->getImageUri($processed), + ]; + } catch (\Throwable $e) { + continue; + } + } + + return $files; + } + + private function resolveNewsUid(string $rawValue): int + { + $rawValue = trim($rawValue); + if ($rawValue === '') { + return 0; + } + + if (ctype_digit($rawValue)) { + return (int)$rawValue; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_news_domain_model_news'); + + $row = $queryBuilder + ->select('uid') + ->from('tx_news_domain_model_news') + ->where( + $queryBuilder->expr()->eq('path_segment', $queryBuilder->createNamedParameter($rawValue, ParameterType::STRING)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + return (int)($row['uid'] ?? 0); + } + + /** + * @return list + */ + private function parseUidList(string $rawValue): array + { + $values = array_filter(array_map('trim', explode(',', $rawValue))); + + return array_values(array_filter(array_map(static fn (string $value): int => (int)$value, $values))); + } + + /** + * @param list $categoryUids + * @return list + */ + private function expandCategoryUids(array $categoryUids): array + { + if ($categoryUids === []) { + return []; + } + + $allUids = array_fill_keys($categoryUids, true); + $queue = $categoryUids; + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + while ($queue !== []) { + $currentUid = array_shift($queue); + if ($currentUid === null) { + continue; + } + + $children = $queryBuilder + ->select('uid') + ->from('sys_category') + ->where( + $queryBuilder->expr()->eq('parent', $queryBuilder->createNamedParameter($currentUid, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('deleted', 0) + ) + ->executeQuery() + ->fetchFirstColumn(); + + foreach ($children as $childUid) { + $childUid = (int)$childUid; + if ($childUid <= 0 || isset($allUids[$childUid])) { + continue; + } + + $allUids[$childUid] = true; + $queue[] = $childUid; + } + } + + return array_values(array_map('intval', array_keys($allUids))); + } + + private function resolveCurrentPageId(): int + { + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $pageInfo = $request->getAttribute('frontend.page.information'); + if ($pageInfo !== null) { + return (int)$pageInfo->getId(); + } + } + + return (int)($GLOBALS['TSFE']->id ?? 0); + } + + /** + * @param array $contentElement + * @param array $settings + * @return array + */ + private function buildSettingsPayload(array $contentElement, array $settings): array + { + $payload = $settings; + $payload['type'] = (string)($contentElement['CType'] ?? ''); + $payload['detailPid'] = (int)($settings['detailPid'] ?? 0); + $payload['listPid'] = (int)($settings['listPid'] ?? 0); + $payload['templateLayout'] = $this->resolveTemplateLayoutValue((string)($contentElement['CType'] ?? ''), (string)($settings['templateLayout'] ?? '')); + $payload['templateLayoutLabel'] = self::TEMPLATE_LAYOUTS[$payload['templateLayout']] ?? ''; + $payload['templateLayouts'] = array_map( + static fn (string $label, string $key): array => ['key' => $key, 'label' => $label], + self::TEMPLATE_LAYOUTS, + array_keys(self::TEMPLATE_LAYOUTS) + ); + + return $payload; + } + + private function resolveTemplateLayoutValue(string $type, string $value): string + { + $value = trim($value); + if ($value !== '' && isset(self::TEMPLATE_LAYOUTS[$value])) { + return $value; + } + + return self::TEMPLATE_LAYOUT_DEFAULTS[$type] ?? ''; + } + + private function resolveSearchTerm(string $type): string + { + if ($type !== 'news_newssearchresult') { + return ''; + } + + $queryParams = $GLOBALS['TYPO3_REQUEST']?->getQueryParams() ?? []; + $search = $queryParams['tx_news_pi1']['search']['sword'] ?? ''; + + return trim((string)$search); + } +} \ No newline at end of file diff --git a/packages/vitec/Classes/UserFunc/PageJsonLdRenderer.php b/packages/vitec/Classes/UserFunc/PageJsonLdRenderer.php old mode 100755 new mode 100644 index 05482c9..80fc10e --- a/packages/vitec/Classes/UserFunc/PageJsonLdRenderer.php +++ b/packages/vitec/Classes/UserFunc/PageJsonLdRenderer.php @@ -93,6 +93,24 @@ final class PageJsonLdRenderer $hasOrganization ); } + // FAQPage when the page contains at least one vitec_faq element. + $faqItems = $this->collectFaqItems($pageId); + if ($faqItems !== []) { + $nodes[] = $service->buildFaqPage($faqItems, $base); + } + // Event nodes when the page contains a vitec_eventlist element. + foreach ($this->collectUpcomingEvents($pageId) as $eventRow) { + $nodes[] = $service->buildEvent( + $eventRow, + $base, + $this->eventImageUrls((int)$eventRow['uid'], $base, $service), + $hasOrganization + ); + } + + + + return $service->encodeGraph($nodes); } catch (\Throwable $e) { @@ -225,4 +243,171 @@ final class PageJsonLdRenderer return $urls; } + + /** + * Collect question/answer pairs from all visible vitec_faq elements on + * this page (Content Blocks collection child table). + * + * @return list + */ + private function collectFaqItems(int $pageId): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $elements = $qb + ->select('uid') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_faq', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if ($elements === []) { + return []; + } + + $items = []; + foreach ($elements as $element) { + $childQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('vitec_faq_items'); + $rows = $childQb + ->select('*') + ->from('vitec_faq_items') + ->where( + $childQb->expr()->eq('foreign_table_parent_uid', $childQb->createNamedParameter((int)$element['uid'], ParameterType::INTEGER)), + $childQb->expr()->eq('deleted', 0), + $childQb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($rows as $row) { + $items[] = [ + 'question' => (string)($row['question'] ?? $row['vitec_question'] ?? ''), + 'answer' => (string)($row['answer'] ?? $row['vitec_answer'] ?? ''), + ]; + } + } + + return $items; + } catch (\Throwable $e) { + // Table may not exist yet (schema not updated) — skip silently. + return []; + } + } + + + /** + * Upcoming event rows when this page contains at least one visible + * vitec_eventlist element. Empty array otherwise. + * + * @return list> + */ + private function collectUpcomingEvents(int $pageId): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $eventlistElement = $qb + ->select('uid', 'pi_flexform') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_eventlist', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$eventlistElement) { + return []; + } + + // Mirror the plugin's "days in advance" window so structured data + // matches what the page actually shows. + $daysInAdvance = 0; + $flexFormService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\FlexFormService::class); + $flexSettings = $flexFormService->convertFlexFormContentToArray((string)($eventlistElement['pi_flexform'] ?? ''))['settings'] ?? []; + $daysInAdvance = (int)($flexSettings['daysinadvance'] ?? 0); + + $todayMidnight = strtotime('today'); + $eventQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_event'); + $expr = $eventQb->expr(); + + $eventQb + ->select('*') + ->from('tx_vitec_domain_model_event') + ->where( + $expr->eq('deleted', 0), + $expr->eq('hidden', 0), + $expr->eq('hideonwebsite', 0), + $expr->or( + $expr->gte('eventend', $eventQb->createNamedParameter($todayMidnight, ParameterType::INTEGER)), + $expr->and( + $expr->eq('eventend', 0), + $expr->gte('eventstart', $eventQb->createNamedParameter($todayMidnight, ParameterType::INTEGER)) + ) + ) + ) + ->orderBy('eventstart', 'ASC'); + + if ($daysInAdvance > 0) { + $eventQb->andWhere( + $expr->lt('eventstart', $eventQb->createNamedParameter($todayMidnight + ($daysInAdvance * 86400), ParameterType::INTEGER)) + ); + } + + return $eventQb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + // Table may not exist yet — skip silently. + return []; + } + } + + /** + * Root-relative logo URLs for an event (fieldname=image). + * + * @return list + */ + private function eventImageUrls(int $eventUid, string $base, $service): array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $rows = $qb + ->select('f.identifier') + ->from('sys_file_reference', 'fr') + ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') + ->where( + $qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_event', ParameterType::STRING)), + $qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('image', ParameterType::STRING)), + $qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($eventUid, ParameterType::INTEGER)), + $qb->expr()->eq('fr.deleted', 0), + $qb->expr()->eq('f.missing', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + $urls = []; + foreach ($rows as $row) { + $identifier = (string)($row['identifier'] ?? ''); + if ($identifier !== '') { + $urls[] = '/fileadmin' . $identifier; + } + } + return $urls; + } catch (\Throwable $e) { + return []; + } + } + } diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php old mode 100755 new mode 100644 index 0221e00..a413495 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -9,6 +9,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; use Evomedien\Vitec\Domain\Repository\ProductRepository; use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\Resource\FileReference; @@ -141,6 +142,7 @@ class ProductListJsonRenderer 'categoryUids' => $categoryUids, 'productCount' => count($productsData), 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromProducts($productsData), ], ]); } @@ -413,6 +415,168 @@ class ProductListJsonRenderer }, $categories); } + private function getDownloadFileType(int $downloadUid): string + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.filetype') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($categories as $category) { + $filetype = trim((string)($category['filetype'] ?? '')); + if ($filetype !== '') { + return $filetype; + } + } + + return ''; + } + + /** + * @return array|null + */ + private function resolveFileByConvention(string $fileprefix, string $filetype): ?array + { + $fileprefix = trim($fileprefix); + $filetype = trim($filetype); + if ($fileprefix === '' || $filetype === '') { + return null; + } + + $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; + if (!is_dir($baseDir) || !is_readable($baseDir)) { + return null; + } + + $escapedPrefix = preg_quote($fileprefix, '/'); + $escapedType = preg_quote($filetype, '/'); + $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; + + $bestFile = null; + $bestNumber = -1; + $bestLetterRank = -1; + + $entries = scandir($baseDir); + if ($entries === false) { + return null; + } + + foreach ($entries as $entry) { + if (!is_string($entry) || $entry === '.' || $entry === '..') { + continue; + } + + if (!preg_match($pattern, $entry, $matches)) { + continue; + } + + $number = (int)$matches[1]; + $letterRank = $this->letterSequenceToRank((string)$matches[2]); + + if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { + $bestNumber = $number; + $bestLetterRank = $letterRank; + $bestFile = $entry; + } + } + + if ($bestFile === null) { + return null; + } + + $fullPath = $baseDir . '/' . $bestFile; + if (!is_file($fullPath) || !is_readable($fullPath)) { + return null; + } + + $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); + $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; + + return [ + 'uid' => 0, + 'name' => $bestFile, + 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), + 'size' => (int)(filesize($fullPath) ?: 0), + 'extension' => $extension, + 'mimeType' => $mimeType, + ]; + } + + private function letterSequenceToRank(string $letters): int + { + $rank = 0; + $letters = strtoupper($letters); + $length = strlen($letters); + + for ($i = 0; $i < $length; $i++) { + $char = ord($letters[$i]); + if ($char < 65 || $char > 90) { + continue; + } + + $rank = ($rank * 26) + ($char - 64); + } + + return $rank; + } + + /** + * @param array> $products + * @return array> + */ + private function collectFallbackFilesFromProducts(array $products): array + { + $fallbackFiles = []; + + foreach ($products as $product) { + $downloads = $product['downloads'] ?? null; + if (!is_array($downloads)) { + continue; + } + + foreach ($downloads as $download) { + if (!is_array($download)) { + continue; + } + + $file = $download['file'] ?? null; + if (!is_array($file) || (int)($file['uid'] ?? 1) !== 0) { + continue; + } + + $fallbackFiles[] = [ + 'productUid' => (int)($product['uid'] ?? 0), + 'downloadUid' => (int)($download['uid'] ?? 0), + 'name' => (string)($file['name'] ?? ''), + 'url' => (string)($file['url'] ?? ''), + 'source' => 'naming-convention-fallback', + ]; + } + } + + return $fallbackFiles; + } + protected function getProductDownloads(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) @@ -440,6 +604,9 @@ class ProductListJsonRenderer $result = []; foreach ($downloads as $download) { $fileInfo = null; + $downloadUid = (int)$download['uid']; + $fileprefix = (string)($download['fileprefix'] ?? ''); + $filetype = $this->getDownloadFileType($downloadUid); if (!empty($download['file'])) { $fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) @@ -450,7 +617,7 @@ class ProductListJsonRenderer ->from('sys_file_reference', 'fr') ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') ->where( - $fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter((int)$download['uid'], ParameterType::INTEGER)), + $fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter($downloadUid, ParameterType::INTEGER)), $fileQueryBuilder->expr()->eq('fr.tablenames', $fileQueryBuilder->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)), $fileQueryBuilder->expr()->eq('fr.fieldname', $fileQueryBuilder->createNamedParameter('file', ParameterType::STRING)), $fileQueryBuilder->expr()->eq('fr.deleted', 0), @@ -473,14 +640,20 @@ class ProductListJsonRenderer } } + if ($fileInfo === null) { + $fileInfo = $this->resolveFileByConvention($fileprefix, $filetype); + } + $result[] = [ - 'uid' => (int)$download['uid'], + 'uid' => $downloadUid, 'title' => $download['title'] ?? '', 'slug' => $download['slug'] ?? '', 'teaser' => $download['teaser'] ?? '', 'description' => $download['description'] ?? '', 'keywords' => $download['keywords'] ?? '', 'icon' => $download['icon'] ?? '', + 'fileprefix' => $fileprefix, + 'filetype' => $filetype, 'file' => $fileInfo, ]; } diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php old mode 100755 new mode 100644 index 4ba8f80..2414b82 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -7,6 +7,7 @@ namespace Evomedien\Vitec\UserFunc; use Evomedien\Vitec\Service\StructuredDataService; use TYPO3\CMS\Core\Attribute\AsAllowedCallable; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -160,6 +161,7 @@ class ProductShowJsonRenderer 'productUid' => $productUid, 'layout' => $layout, 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromDownloads((array)($response['product']['downloads'] ?? [])), ]; } @@ -495,6 +497,160 @@ class ProductShowJsonRenderer }, $categories); } + private function getDownloadFileType(int $downloadUid): string + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + + $categories = $qb + ->select('c.filetype') + ->from('sys_category', 'c') + ->join( + 'c', + 'sys_category_record_mm', + 'mm', + 'mm.uid_local = c.uid AND mm.tablenames = ' . + $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('c.deleted', 0), + $qb->expr()->eq('c.hidden', 0), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($categories as $category) { + $filetype = trim((string)($category['filetype'] ?? '')); + if ($filetype !== '') { + return $filetype; + } + } + + return ''; + } + + /** + * @return array|null + */ + private function resolveFileByConvention(string $fileprefix, string $filetype): ?array + { + $fileprefix = trim($fileprefix); + $filetype = trim($filetype); + if ($fileprefix === '' || $filetype === '') { + return null; + } + + $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; + if (!is_dir($baseDir) || !is_readable($baseDir)) { + return null; + } + + $escapedPrefix = preg_quote($fileprefix, '/'); + $escapedType = preg_quote($filetype, '/'); + $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; + + $bestFile = null; + $bestNumber = -1; + $bestLetterRank = -1; + + $entries = scandir($baseDir); + if ($entries === false) { + return null; + } + + foreach ($entries as $entry) { + if (!is_string($entry) || $entry === '.' || $entry === '..') { + continue; + } + + if (!preg_match($pattern, $entry, $matches)) { + continue; + } + + $number = (int)$matches[1]; + $letterRank = $this->letterSequenceToRank((string)$matches[2]); + + if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { + $bestNumber = $number; + $bestLetterRank = $letterRank; + $bestFile = $entry; + } + } + + if ($bestFile === null) { + return null; + } + + $fullPath = $baseDir . '/' . $bestFile; + if (!is_file($fullPath) || !is_readable($fullPath)) { + return null; + } + + $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); + $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; + + return [ + 'uid' => 0, + 'name' => $bestFile, + 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), + 'size' => (int)(filesize($fullPath) ?: 0), + 'extension' => $extension, + 'mimeType' => $mimeType, + ]; + } + + private function letterSequenceToRank(string $letters): int + { + $rank = 0; + $letters = strtoupper($letters); + $length = strlen($letters); + + for ($i = 0; $i < $length; $i++) { + $char = ord($letters[$i]); + if ($char < 65 || $char > 90) { + continue; + } + + $rank = ($rank * 26) + ($char - 64); + } + + return $rank; + } + + /** + * @param array> $downloads + * @return array> + */ + private function collectFallbackFilesFromDownloads(array $downloads): array + { + $fallbackFiles = []; + + foreach ($downloads as $download) { + if (!is_array($download)) { + continue; + } + + $file = $download['file'] ?? null; + if (!is_array($file) || (int)($file['uid'] ?? 1) !== 0) { + continue; + } + + $fallbackFiles[] = [ + 'downloadUid' => (int)($download['uid'] ?? 0), + 'name' => (string)($file['name'] ?? ''), + 'url' => (string)($file['url'] ?? ''), + 'source' => 'naming-convention-fallback', + ]; + } + + return $fallbackFiles; + } + protected function getProductDownloads(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) @@ -522,6 +678,9 @@ class ProductShowJsonRenderer $result = []; foreach ($downloads as $download) { $fileInfo = null; + $downloadUid = (int)$download['uid']; + $fileprefix = (string)($download['fileprefix'] ?? ''); + $filetype = $this->getDownloadFileType($downloadUid); if (!empty($download['file'])) { $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) @@ -532,7 +691,7 @@ class ProductShowJsonRenderer ->from('sys_file_reference', 'fr') ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') ->where( - $fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter((int)$download['uid'], ParameterType::INTEGER)), + $fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter($downloadUid, ParameterType::INTEGER)), $fileQueryBuilder->expr()->eq('fr.tablenames', $fileQueryBuilder->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)), $fileQueryBuilder->expr()->eq('fr.fieldname', $fileQueryBuilder->createNamedParameter('file', ParameterType::STRING)), $fileQueryBuilder->expr()->eq('fr.deleted', 0), @@ -555,14 +714,20 @@ class ProductShowJsonRenderer } } + if ($fileInfo === null) { + $fileInfo = $this->resolveFileByConvention($fileprefix, $filetype); + } + $result[] = [ - 'uid' => (int)$download['uid'], + 'uid' => $downloadUid, 'title' => $download['title'] ?? '', 'slug' => $download['slug'] ?? '', 'teaser' => $download['teaser'] ?? '', 'description' => $download['description'] ?? '', 'keywords' => $download['keywords'] ?? '', 'icon' => $download['icon'] ?? '', + 'fileprefix' => $fileprefix, + 'filetype' => $filetype, 'file' => $fileInfo, ]; } diff --git a/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php old mode 100755 new mode 100644 index d6e6852..9d1d2b6 --- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php @@ -4,34 +4,33 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; - -use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; +use Evomedien\Vitec\Service\UsecaseSerializer; +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Service\ImageService; /** - * UserFunc to render the usecase list as JSON for headless output. + * UserFunc: render the Success Story list as JSON (headless). * - * `render()` = page discovery (top-level plugin). `renderForRecord()` = - * one specific tt_content row, reused by ContainerChildrenProcessor for - * usecase-list plugins nested in a b13 container. Exception-safe. + * `render()` = top-level plugin / page discovery. `renderForRecord()` = one + * specific tt_content row (reused by ContainerChildrenProcessor for nested + * usecase-list plugins). All serialisation is delegated to UsecaseSerializer. + * Exception-safe. */ class UsecaseListJsonRenderer { + private const TABLE = 'tx_vitec_domain_model_usecase'; + #[AsAllowedCallable] public function render(string $content, array $conf): string { - // 1) cObj data path $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaselist') { return $this->renderForRecord($row); } - // 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context) $pageId = 0; $request = $GLOBALS['TYPO3_REQUEST'] ?? null; if ($request !== null) { @@ -47,29 +46,26 @@ class UsecaseListJsonRenderer return ''; } - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $ces = $qb ->select('*') ->from('tt_content') ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaselist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAllAssociative(); - if (empty($contentElements)) { + if (empty($ces)) { return ''; } - return $this->renderForRecord($contentElements[0]); + return $this->renderForRecord($ces[0]); } - /** * @param array $contentElement */ @@ -81,167 +77,36 @@ class UsecaseListJsonRenderer $settings = $flexFormData['settings'] ?? []; $debugMode = (bool)($settings['debug'] ?? false); - $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); - - $usecases = $usecaseQb - ->select('u.*') - ->from('tx_vitec_domain_model_usecase', 'u') + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE); + $rows = $qb + ->select('*') + ->from(self::TABLE) ->where( - $usecaseQb->expr()->eq('u.deleted', 0), - $usecaseQb->expr()->eq('u.hidden', 0), - $usecaseQb->expr()->eq('u.hideonwebsite', 0) + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0), + $qb->expr()->eq('hideonwebsite', 0) ) - ->orderBy('u.title', 'ASC') + ->orderBy('featured', 'DESC') + ->addOrderBy('title', 'ASC') ->executeQuery() ->fetchAllAssociative(); - $usecasesData = []; - foreach ($usecases as $usecase) { - $usecasesData[] = $this->serializeUsecase($usecase); - } + $serializer = GeneralUtility::makeInstance(UsecaseSerializer::class); + $usecases = array_map( + static fn (array $u): array => $serializer->serializeListItem($u), + $rows + ); if ($debugMode) { - return json_encode([ - 'usecases' => $usecasesData, - 'debug' => [ - 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), - 'usecaseCount' => count($usecasesData), - 'settings' => $settings, - ], + return (string)json_encode([ + 'usecases' => $usecases, + 'debug' => ['count' => count($usecases), 'settings' => $settings], ]); } - return json_encode($usecasesData); + return (string)json_encode($usecases); } catch (\Throwable $e) { return ''; } } - - /** - * @param array $usecase - * @return array - */ - protected function serializeUsecase(array $usecase): array - { - $uid = (int)$usecase['uid']; - - return [ - 'uid' => $uid, - 'title' => (string)($usecase['title'] ?? ''), - 'slug' => (string)($usecase['slug'] ?? ''), - 'subtitle' => (string)($usecase['subtitle'] ?? ''), - 'teaser' => (string)($usecase['teaser'] ?? ''), - 'description' => (string)($usecase['description'] ?? ''), - 'singlepid' => (string)($usecase['singlepid'] ?? ''), - 'hideonapp' => (bool)($usecase['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false), - 'categories' => $this->getUsecaseCategories($uid), - 'caseimage' => $this->getUsecaseImage($uid, 'caseimage'), - 'logoimage' => $this->getUsecaseImage($uid, 'logoimage'), - ]; - } - - /** - * @return array|null - */ - protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRefData = $queryBuilder - ->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop') - ->from('sys_file_reference', 'sfr') - ->where( - $queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$fileRefData) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $variant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width, 'crop' => $fileRefData['crop'] ?? null] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($variant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $default = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($default), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - protected function getUsecaseCategories(int $usecaseUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_category'); - - $categories = $queryBuilder - ->select('c.uid', 'c.title', 'c.description') - ->from('sys_category', 'c') - ->join( - 'c', - 'sys_category_record_mm', - 'mm', - 'mm.uid_local = c.uid AND mm.tablenames = ' . - $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(static function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'] ?? '', - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } } diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php old mode 100755 new mode 100644 index 49459ec..2d0925c --- a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php @@ -4,34 +4,32 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; - -use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; +use Evomedien\Vitec\Service\UsecaseSerializer; +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Service\ImageService; /** - * UserFunc to render a single usecase as JSON for headless output. + * UserFunc: render a single Success Story as JSON (headless). * - * `render()` = page discovery (top-level plugin). `renderForRecord()` = - * one specific tt_content row, reused by ContainerChildrenProcessor for - * usecase-show plugins nested in a b13 container. Exception-safe. + * The story is chosen from the plugin FlexForm (settings.usecase) or, when + * empty, from the request param tx_vitec_usecaseshow[usecase] (uid or slug). + * Full serialisation is delegated to UsecaseSerializer. Exception-safe. */ class UsecaseShowJsonRenderer { + private const TABLE = 'tx_vitec_domain_model_usecase'; + #[AsAllowedCallable] public function render(string $content, array $conf): string { - // 1) cObj data path $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaseshow') { return $this->renderForRecord($row); } - // 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context) $pageId = 0; $request = $GLOBALS['TYPO3_REQUEST'] ?? null; if ($request !== null) { @@ -47,243 +45,104 @@ class UsecaseShowJsonRenderer return ''; } - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $ces = $qb ->select('*') ->from('tt_content') ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAllAssociative(); - if (empty($contentElements)) { + if (empty($ces)) { return ''; } - return $this->renderForRecord($contentElements[0]); + return $this->renderForRecord($ces[0]); } - /** * @param array $contentElement */ public function renderForRecord(array $contentElement): string { try { - $pageId = (int)($GLOBALS['TSFE']->id ?? 0); - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); $settings = $flexFormData['settings'] ?? []; - $usecaseUid = (int)($settings['usecase'] ?? 0); - $layout = (string)($settings['layout'] ?? 'default'); + $layout = (string)($settings['layout'] ?? 'default'); $debugMode = (bool)($settings['debug'] ?? false); + $usecaseUid = (int)($settings['usecase'] ?? 0); + // Fallback: uid/slug from the request (React detail route). if (!$usecaseUid) { - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $usecaseParam = $routeParams['tx_vitec_usecaseshow']['usecase'] ?? null; - - if ($usecaseParam) { - if (!is_numeric($usecaseParam)) { - $slugQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); - $bySlug = $slugQb - ->select('uid') - ->from('tx_vitec_domain_model_usecase') - ->where( - $slugQb->expr()->eq('slug', $slugQb->createNamedParameter($usecaseParam)), - $slugQb->expr()->eq('deleted', 0), - $slugQb->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - $usecaseUid = (int)($bySlug['uid'] ?? 0); - } else { - $usecaseUid = (int)$usecaseParam; - } + $params = $GLOBALS['TYPO3_REQUEST']?->getQueryParams() ?? []; + $param = $params['tx_vitec_usecaseshow']['usecase'] ?? null; + if ($param !== null && $param !== '') { + $usecaseUid = is_numeric($param) ? (int)$param : $this->resolveSlug((string)$param); } } if (!$usecaseUid) { return $debugMode - ? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]]) + ? (string)json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]]) : ''; } - $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); - - $usecase = $usecaseQb + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE); + $usecase = $qb ->select('*') - ->from('tx_vitec_domain_model_usecase') + ->from(self::TABLE) ->where( - $usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $usecaseQb->expr()->eq('deleted', 0), - $usecaseQb->expr()->eq('hidden', 0) + $qb->expr()->eq('uid', $qb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAssociative(); if (!$usecase) { return $debugMode - ? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]]) + ? (string)json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]]) : ''; } + $serializer = GeneralUtility::makeInstance(UsecaseSerializer::class); + $response = [ - 'usecase' => $this->serializeUsecase($usecase), - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], + 'usecase' => $serializer->serializeDetail($usecase), + 'layout' => $layout, ]; if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'usecaseUid' => $usecaseUid, - 'layout' => $layout, - 'settings' => $settings, - ]; + $response['debug'] = ['usecaseUid' => $usecaseUid, 'layout' => $layout, 'settings' => $settings]; } - return json_encode($response); + return (string)json_encode($response); } catch (\Throwable $e) { return ''; } } - /** - * @param array $usecase - * @return array - */ - protected function serializeUsecase(array $usecase): array + private function resolveSlug(string $slug): int { - $uid = (int)$usecase['uid']; - - return [ - 'uid' => $uid, - 'title' => (string)($usecase['title'] ?? ''), - 'slug' => (string)($usecase['slug'] ?? ''), - 'subtitle' => (string)($usecase['subtitle'] ?? ''), - 'teaser' => (string)($usecase['teaser'] ?? ''), - 'description' => (string)($usecase['description'] ?? ''), - 'singlepid' => (string)($usecase['singlepid'] ?? ''), - 'hideonapp' => (bool)($usecase['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false), - 'categories' => $this->getUsecaseCategories($uid), - 'caseimage' => $this->getUsecaseImage($uid, 'caseimage'), - 'logoimage' => $this->getUsecaseImage($uid, 'logoimage'), - ]; - } - - /** - * @return array|null - */ - protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRefData = $queryBuilder - ->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop') - ->from('sys_file_reference', 'sfr') + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE); + $row = $qb + ->select('uid') + ->from(self::TABLE) ->where( - $queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) + $qb->expr()->eq('slug', $qb->createNamedParameter($slug)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) ) - ->orderBy('sfr.sorting_foreign') - ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); - if (!$fileRefData) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $variant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width, 'crop' => $fileRefData['crop'] ?? null] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($variant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $default = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($default), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - protected function getUsecaseCategories(int $usecaseUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_category'); - - $categories = $queryBuilder - ->select('c.uid', 'c.title', 'c.description') - ->from('sys_category', 'c') - ->join( - 'c', - 'sys_category_record_mm', - 'mm', - 'mm.uid_local = c.uid AND mm.tablenames = ' . - $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(static function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'] ?? '', - 'description' => $cat['description'] ?? '', - ]; - }, $categories); + return (int)($row['uid'] ?? 0); } } diff --git a/packages/vitec/Classes/View/BackendLayoutDataProvider.php b/packages/vitec/Classes/View/BackendLayoutDataProvider.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Classes/View/VitecBackendLayoutView.php b/packages/vitec/Classes/View/VitecBackendLayoutView.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/Backend/AjaxRoutes.php b/packages/vitec/Configuration/Backend/AjaxRoutes.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/FlexForms/CardsCarousel.xml b/packages/vitec/Configuration/FlexForms/CardsCarousel.xml new file mode 100644 index 0000000..e9d86f1 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/CardsCarousel.xml @@ -0,0 +1,96 @@ + + + + + + Carousel Settings + array + + + + + select + selectSingle + + + + 4 + + + + 3 + + + + 2 + + + + 1 + + + 4 + + + + + + + check + 1 + + + + + + + check + 1 + + + + + + + check + 0 + + + + + + + check + 0 + + + + + + + number + 5000 + + + + + + + input + 30 + trim + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Container.xml b/packages/vitec/Configuration/FlexForms/Container.xml old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/FlexForms/Downloadcard.xml b/packages/vitec/Configuration/FlexForms/Downloadcard.xml old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/FlexForms/Eventlist.xml b/packages/vitec/Configuration/FlexForms/Eventlist.xml new file mode 100644 index 0000000..c6e1b36 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Eventlist.xml @@ -0,0 +1,48 @@ + + + + 1 + + + + + Event List Settings + array + + + + Only show upcoming events starting within the next N days, e.g. 60. + + number + 0 + + + + + + + check + 0 + + + + + + + number + 0 + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/Icons.php b/packages/vitec/Configuration/Icons.php old mode 100755 new mode 100644 index 0925a62..b3fdf59 --- a/packages/vitec/Configuration/Icons.php +++ b/packages/vitec/Configuration/Icons.php @@ -32,6 +32,10 @@ return [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaselist.svg', ], + 'vitec-plugin-eventlist' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-eventlist.svg', + ], 'vitec-plugin-downloadcard' => [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcard.svg', diff --git a/packages/vitec/Configuration/JavaScriptModules.php b/packages/vitec/Configuration/JavaScriptModules.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/Sets/Vitecset/config.yaml b/packages/vitec/Configuration/Sets/Vitecset/config.yaml old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript old mode 100755 new mode 100644 index e77c74b..6bc1a58 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript @@ -20,7 +20,7 @@ config { # ============================================================================= tt_content { - vitec_productlist < lib.contentElement + vitec_productlist < lib.contentElementWithHeader vitec_productlist { fields { content { @@ -32,7 +32,28 @@ tt_content { } } - vitec_productshow < lib.contentElement + news_pi1 < lib.contentElementWithHeader + news_pi1 { + fields { + content { + fields { + news = USER + news.userFunc = Evomedien\Vitec\UserFunc\NewsJsonRenderer->render + } + } + } + } + + news_newsliststicky < tt_content.news_pi1 + news_newsselectedlist < tt_content.news_pi1 + news_newsdetail < tt_content.news_pi1 + news_newsdatemenu < tt_content.news_pi1 + news_categorylist < tt_content.news_pi1 + news_newssearchform < tt_content.news_pi1 + news_newssearchresult < tt_content.news_pi1 + news_taglist < tt_content.news_pi1 + + vitec_productshow < lib.contentElementWithHeader vitec_productshow { fields { content { @@ -44,7 +65,7 @@ tt_content { } } - vitec_usecaselist < lib.contentElement + vitec_usecaselist < lib.contentElementWithHeader vitec_usecaselist { fields { content { @@ -56,7 +77,7 @@ tt_content { } } - vitec_usecaseshow < lib.contentElement + vitec_usecaseshow < lib.contentElementWithHeader vitec_usecaseshow { fields { content { @@ -68,7 +89,7 @@ tt_content { } } - vitec_marketshow < lib.contentElement + vitec_marketshow < lib.contentElementWithHeader vitec_marketshow { fields { content { @@ -80,7 +101,7 @@ tt_content { } } - vitec_solutionshow < lib.contentElement + vitec_solutionshow < lib.contentElementWithHeader vitec_solutionshow { fields { content { @@ -92,7 +113,7 @@ tt_content { } } - vitec_downloadcard < lib.contentElement + vitec_downloadcard < lib.contentElementWithHeader vitec_downloadcard { fields { content { @@ -104,7 +125,7 @@ tt_content { } } - vitec_downloadcardcollection < lib.contentElement + vitec_downloadcardcollection < lib.contentElementWithHeader vitec_downloadcardcollection { fields { content { @@ -116,7 +137,7 @@ tt_content { } } - vitec_datasheets < lib.contentElement + vitec_datasheets < lib.contentElementWithHeader vitec_datasheets { fields { content { @@ -127,6 +148,18 @@ tt_content { } } } + + vitec_eventlist =< lib.contentElementWithHeader + vitec_eventlist { + fields { + content { + fields { + eventlist = USER + eventlist.userFunc = Evomedien\Vitec\UserFunc\EventlistJsonRenderer->render + } + } + } + } } # Include container (layout) rendering definitions diff --git a/packages/vitec/Configuration/TCA/Overrides/pages.php b/packages/vitec/Configuration/TCA/Overrides/pages.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content.php b/packages/vitec/Configuration/TCA/Overrides/tt_content.php index ac8a692..119452a 100644 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content.php @@ -34,6 +34,7 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility; $registerPluginWithFlexForm('Downloadcard', 'Download Card', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcard.xml', 'vitec-plugin-downloadcard'); $registerPluginWithFlexForm('Downloadcardcollection', 'Download Card Collection', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml', 'vitec-plugin-downloadcardcollection'); $registerPluginWithFlexForm('Datasheets', 'Datasheets', 'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml'); + $registerPluginWithFlexForm('Eventlist', 'Event List', 'FILE:EXT:vitec/Configuration/FlexForms/Eventlist.xml', 'vitec-plugin-eventlist'); // Change frame_class to allow multiple selections. $GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox'; @@ -50,4 +51,9 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility; ['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'], ] ); + + // Inline parent pointer: content elements owned by a Success Story record. + $GLOBALS['TCA']['tt_content']['columns']['tx_vitec_usecase_content'] = [ + 'config' => ['type' => 'passthrough'], + ]; })(); diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cards_carousel.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cards_carousel.php new file mode 100644 index 0000000..99c28ce --- /dev/null +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cards_carousel.php @@ -0,0 +1,63 @@ +configureContainer( + (new ContainerConfiguration( + 'vitec_cards_carousel', + $l . 'cards_carousel.title', + $l . 'cards_carousel.description', + [ + [ + ['name' => $l . 'cards_carousel.column', 'colPos' => 260], + ], + ] + )) + ->setIcon('EXT:vitec/Resources/Public/Icons/vitec-cards-carousel.svg') + ->setGroup('vitec_custom_components') + ->setSaveAndCloseInNewContentElementWizard(true) + ); + + $GLOBALS['TCA']['tt_content']['types']['vitec_cards_carousel']['showitem'] = + '--palette--;;general, + --palette--;;headers, + tx_vitec_bg_variant, + pi_flexform;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cards_carousel.flexform.label, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;;frames, + --palette--;;appearanceLinks, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, + --palette--;;language, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, + --palette--;;hidden, + --palette--;;access'; + + ExtensionManagementUtility::addPiFlexFormValue( + '*', + 'FILE:EXT:vitec/Configuration/FlexForms/CardsCarousel.xml', + 'vitec_cards_carousel' + ); + + $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG' +mod.wizards.newContentElement.wizardItems.vitec_custom_components.elements.vitec_cards_carousel { + iconIdentifier = vitec-cards-carousel + title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cards_carousel.title + description = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cards_carousel.description + tt_content_defValues { + CType = vitec_cards_carousel + } +} +TSCONFIG; +})(); diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_25_25_25_25.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_25_25_25_25.php old mode 100755 new mode 100644 index e35ed3f..83bcb18 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_25_25_25_25.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_25_25_25_25.php @@ -35,10 +35,14 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_cols_25_25_25_25']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:palette.grid;vitec_grid, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.1;vitec_col1_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.2;vitec_col2_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.3;vitec_col3_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.4;vitec_col4_flex, --palette--;;frames, --palette--;;appearanceLinks, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_33_33.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_33_33.php old mode 100755 new mode 100644 index 84f26a1..d537a95 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_33_33.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_33_33.php @@ -34,10 +34,13 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_cols_33_33_33']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:palette.grid;vitec_grid, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.1;vitec_col1_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.2;vitec_col2_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.3;vitec_col3_flex, --palette--;;frames, --palette--;;appearanceLinks, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_66.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_66.php old mode 100755 new mode 100644 index f068f00..299049a --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_66.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_33_66.php @@ -33,10 +33,12 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_cols_33_66']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:palette.grid;vitec_grid, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.sidebar_33;vitec_col1_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.main_66;vitec_col2_flex, --palette--;;frames, --palette--;;appearanceLinks, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_50_50.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_50_50.php old mode 100755 new mode 100644 index a183cf9..25062d5 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_50_50.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_50_50.php @@ -33,10 +33,12 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_cols_50_50']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:palette.grid;vitec_grid, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.1;vitec_col1_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.2;vitec_col2_flex, --palette--;;frames, --palette--;;appearanceLinks, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_66_33.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_66_33.php old mode 100755 new mode 100644 index a387c9d..b0df6dd --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_66_33.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_cols_66_33.php @@ -33,10 +33,12 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_cols_66_33']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:palette.grid;vitec_grid, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.main_66;vitec_col1_flex, + --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:column.sidebar_33;vitec_col2_flex, --palette--;;frames, --palette--;;appearanceLinks, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php old mode 100755 new mode 100644 index 1c1f031..3a8088f --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php @@ -32,8 +32,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; $GLOBALS['TCA']['tt_content']['types']['vitec_container']['showitem'] = '--palette--;;general, - header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading, - subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline, + --palette--;;headers, tx_vitec_bg_variant, pi_flexform;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.label, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php old mode 100755 new mode 100644 index 75a962a..654b6a6 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php @@ -27,6 +27,87 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; ], ]); + // ------------------------------------------------------------------------- + // Shared parent-level setting: column gap scale (1–5) for the whole grid. + // Concerns the container itself (not a single column); maps to a spacing + // token in the React frontend. Rendered in the "Grid" palette on top of the + // Appearance tab, above the per-column rows. + // ------------------------------------------------------------------------- + ExtensionManagementUtility::addTCAcolumns('tt_content', [ + 'tx_vitec_gap' => [ + 'label' => $l . 'gap.label', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => $l . 'gap.option.1', 'value' => '1'], + ['label' => $l . 'gap.option.2', 'value' => '2'], + ['label' => $l . 'gap.option.3', 'value' => '3'], + ['label' => $l . 'gap.option.4', 'value' => '4'], + ['label' => $l . 'gap.option.5', 'value' => '5'], + ], + 'default' => '3', + ], + ], + ]); + + // Parent-element palette: settings that concern the whole grid, not a column. + $GLOBALS['TCA']['tt_content']['palettes']['vitec_grid'] = [ + 'showitem' => 'tx_vitec_gap', + ]; + + // ------------------------------------------------------------------------- + // Shared per-column flex alignment fields for the X-column containers. + // Positional: col1 = first column, col2 = second, ... (max 4 columns). + // "Align" -> CSS align-items (cross-axis alignment of the column's items) + // "Justify" -> CSS justify-content (main-axis distribution within the column) + // Reused across vitec_cols_50_50 / 33_66 / 66_33 / 33_33_33 / 25_25_25_25. + // ------------------------------------------------------------------------- + $alignItems = [ + ['label' => $l . 'flex.align.flex_start', 'value' => 'flex-start'], + ['label' => $l . 'flex.align.center', 'value' => 'center'], + ['label' => $l . 'flex.align.flex_end', 'value' => 'flex-end'], + ['label' => $l . 'flex.align.stretch', 'value' => 'stretch'], + ]; + $justifyItems = [ + ['label' => $l . 'flex.justify.flex_start', 'value' => 'flex-start'], + ['label' => $l . 'flex.justify.center', 'value' => 'center'], + ['label' => $l . 'flex.justify.space_between', 'value' => 'space-between'], + ['label' => $l . 'flex.justify.space_around', 'value' => 'space-around'], + ['label' => $l . 'flex.justify.space_evenly', 'value' => 'space-evenly'], + ]; + + $columnFields = []; + for ($i = 1; $i <= 4; $i++) { + $columnFields['tx_vitec_col' . $i . '_align'] = [ + 'label' => $l . 'flex.align.label', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => $alignItems, + 'default' => 'stretch', + ], + ]; + $columnFields['tx_vitec_col' . $i . '_justify'] = [ + 'label' => $l . 'flex.justify.label', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => $justifyItems, + 'default' => 'flex-start', + ], + ]; + } + ExtensionManagementUtility::addTCAcolumns('tt_content', $columnFields); + + // One palette per column: Align + Justify on a single row. The visible row + // label is overridden per container in its showitem (--palette--;;). + for ($i = 1; $i <= 4; $i++) { + $GLOBALS['TCA']['tt_content']['palettes']['vitec_col' . $i . '_flex'] = [ + 'showitem' => 'tx_vitec_col' . $i . '_align, tx_vitec_col' . $i . '_justify', + ]; + } + // Register VITEC wizard group header $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG' mod.wizards.newContentElement.wizardItems.vitec { diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_event.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_event.php new file mode 100644 index 0000000..933dafd --- /dev/null +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_event.php @@ -0,0 +1,290 @@ + [ + 'title' => 'VITEC Event', + 'label' => 'title', + 'label_alt' => 'city,eventstart', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'delete' => 'deleted', + 'default_sortby' => 'eventstart ASC', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'searchFields' => 'title,teaser,venue,booth,city,country', + 'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_event.svg', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + ], + 'types' => [ + '1' => [ + 'showitem' => 'title, slug, teaser, description, + --div--;Event Details, eventstart, eventend, venue, booth, city, country, attendancemode, eventstatus, + --div--;Links and Media, eventurl, meetinglink, image, + --div--;Visibility, hideonwebsite, hideonapp, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime', + ], + ], + 'columns' => [ + 'sys_language_uid' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', + 'config' => [ + 'type' => 'language', + ], + ], + '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', + 'default' => 0, + 'items' => [ + ['label' => '', 'value' => 0], + ], + 'foreign_table' => 'tx_vitec_domain_model_event', + 'foreign_table_where' => 'AND {#tx_vitec_domain_model_event}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_event}.{#sys_language_uid} IN (-1,0)', + ], + ], + 'l10n_diffsource' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'hidden' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'items' => [ + [ + 'label' => '', + 'invertStateDisplay' => true, + ], + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime', + 'config' => [ + 'type' => 'datetime', + 'default' => 0, + ], + ], + 'endtime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime', + 'config' => [ + 'type' => 'datetime', + 'default' => 0, + ], + ], + + 'title' => [ + 'exclude' => false, + 'label' => 'Event Name', + 'config' => [ + 'type' => 'input', + 'size' => 40, + 'max' => 255, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'slug' => [ + 'exclude' => true, + 'label' => 'Slug', + 'config' => [ + 'type' => 'slug', + 'size' => 50, + 'generatorOptions' => [ + 'fields' => ['title'], + 'replacements' => ['/' => '-'], + ], + 'fallbackCharacter' => '-', + 'eval' => 'uniqueInSite', + 'default' => '', + ], + ], + 'teaser' => [ + 'exclude' => true, + 'label' => 'Teaser', + 'config' => [ + 'type' => 'input', + 'size' => 40, + 'max' => 255, + 'eval' => 'trim', + ], + ], + 'description' => [ + 'exclude' => true, + 'label' => 'Description', + 'config' => [ + 'type' => 'text', + 'cols' => 40, + 'rows' => 8, + 'enableRichtext' => true, + ], + ], + + 'eventstart' => [ + 'exclude' => false, + 'label' => 'Event Start Date', + 'config' => [ + 'type' => 'datetime', + 'format' => 'date', + 'default' => 0, + 'required' => true, + ], + ], + 'eventend' => [ + 'exclude' => true, + 'label' => 'Event End Date', + 'config' => [ + 'type' => 'datetime', + 'format' => 'date', + 'default' => 0, + ], + ], + 'venue' => [ + 'exclude' => true, + 'label' => 'Venue', + 'description' => 'e.g. "Queen Sirikit National Convention Center (QSNCC)"', + 'config' => [ + 'type' => 'input', + 'size' => 40, + 'max' => 255, + 'eval' => 'trim', + ], + ], + 'booth' => [ + 'exclude' => true, + 'label' => 'Booth / Stand', + 'description' => 'e.g. "Stand H10"', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'max' => 255, + 'eval' => 'trim', + ], + ], + 'city' => [ + 'exclude' => true, + 'label' => 'City', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'max' => 255, + 'eval' => 'trim', + ], + ], + 'country' => [ + 'exclude' => true, + 'label' => 'Country', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'max' => 255, + 'eval' => 'trim', + ], + ], + 'attendancemode' => [ + 'exclude' => true, + 'label' => 'Attendance Mode', + 'description' => 'Used for schema.org structured data (eventAttendanceMode).', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'default' => 'offline', + 'items' => [ + ['label' => 'On-site (offline)', 'value' => 'offline'], + ['label' => 'Online', 'value' => 'online'], + ['label' => 'Hybrid (mixed)', 'value' => 'mixed'], + ], + ], + ], + 'eventstatus' => [ + 'exclude' => true, + 'label' => 'Event Status', + 'description' => 'Used for schema.org structured data (eventStatus).', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'default' => 'scheduled', + 'items' => [ + ['label' => 'Scheduled', 'value' => 'scheduled'], + ['label' => 'Cancelled', 'value' => 'cancelled'], + ['label' => 'Postponed', 'value' => 'postponed'], + ['label' => 'Rescheduled', 'value' => 'rescheduled'], + ['label' => 'Moved Online', 'value' => 'movedonline'], + ], + ], + ], + + 'eventurl' => [ + 'exclude' => true, + 'label' => 'Event Website', + 'config' => [ + 'type' => 'link', + 'allowedTypes' => ['url', 'page'], + ], + ], + 'meetinglink' => [ + 'exclude' => true, + 'label' => 'Book-a-Meeting Link', + 'description' => 'Link or email for the "Book a Meeting" call-to-action.', + 'config' => [ + 'type' => 'link', + 'allowedTypes' => ['url', 'email', 'page'], + ], + ], + 'image' => [ + 'exclude' => true, + 'label' => 'Event Logo / Image', + 'config' => [ + 'type' => 'file', + 'maxitems' => 1, + 'allowed' => 'common-image-types', + ], + ], + + 'hideonwebsite' => [ + 'exclude' => true, + 'label' => 'Hide on Website', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 0, + ], + ], + 'hideonapp' => [ + 'exclude' => true, + 'label' => 'Hide in App', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 0, + ], + ], + + 'categories' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.categories', + 'config' => [ + 'type' => 'category', + ], + ], + ], +]; diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php index 8ac1fd6..3581804 100644 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php @@ -1,6 +1,6 @@ [ + 'ctrl' => [ 'title' => 'VITEC Success Story', 'label' => 'title', 'tstamp' => 'tstamp', @@ -16,303 +16,325 @@ return [ 'starttime' => 'starttime', 'endtime' => 'endtime', ], - 'searchFields' => 'title,slug', + 'searchFields' => 'title,slug,subtitle,teaser,seo_title,seo_description', 'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_usecase.gif', 'security' => [ 'ignorePageTypeRestriction' => true, ], ], - 'types' => [ - '1' => ['showitem' => 'title, slug, subtitle, teaser, description, singlepid, caseimage, logoimage, - --div--;Visibility, hideonapp, hideonwebsite, - --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource, - --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, - --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime', ], + 'types' => [ + '1' => [ + 'showitem' => + '--div--;General, + title, slug, subtitle, + --div--;List view, + card_image, customer_logo, teaser, + --div--;Detail · Hero, + hero_bgimage, hero_small_image, hero_video, hero_overlay_color, hero_overlay_opacity, + --div--;Detail · Content, + content_elements, + --div--;Detail · Related, + market, solutions, products, categories, + --div--;SEO, + seo_title, seo_description, no_index, no_follow, canonical_link, + --palette--;Open Graph;ogPalette, + --palette--;Twitter;twitterPalette, + --div--;Appearance, + layout_variant, background_variant, accent_color, + featured, show_related, hero_layout, text_theme, + --palette--;Visibility;visibilityPalette, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, + sys_language_uid, l10n_parent, l10n_diffsource, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, + hidden, starttime, endtime', + ], ], - 'columns' => [ + 'palettes' => [ + 'ogPalette' => ['showitem' => 'og_title, og_description, --linebreak--, og_image'], + 'twitterPalette' => ['showitem' => 'twitter_title, twitter_description, --linebreak--, twitter_image'], + 'visibilityPalette' => ['showitem' => 'hideonwebsite, hideonapp'], + ], + 'columns' => [ + // ------------------------------------------------------------ system 'sys_language_uid' => [ 'exclude' => true, 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', - 'config' => [ - 'type' => 'language', - ], + 'config' => ['type' => 'language'], ], - 'l10n_parent' => [ + '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', 'default' => 0, - 'items' => [ - ['', 0], - ], - 'foreign_table' => 'tx_vitec_domain_model_download', - 'foreign_table_where' => 'AND {#tx_vitec_domain_model_download}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_download}.{#sys_language_uid} IN (-1,0)', + 'items' => [['label' => '', 'value' => 0]], + 'foreign_table' => 'tx_vitec_domain_model_usecase', + 'foreign_table_where' => 'AND {#tx_vitec_domain_model_usecase}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_usecase}.{#sys_language_uid} IN (-1,0)', ], ], - 'l10n_diffsource' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - 'hidden' => [ + 'l10n_diffsource' => ['config' => ['type' => 'passthrough']], + 'hidden' => [ 'exclude' => true, 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', 'config' => [ 'type' => 'check', 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => true, - ], - ], + 'items' => [['value' => '', 'label' => '', 'invertStateDisplay' => true]], ], ], - 'starttime' => [ + 'starttime' => [ 'exclude' => true, 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'eval' => 'datetime,int', - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], + 'config' => ['type' => 'datetime', 'default' => 0, 'behaviour' => ['allowLanguageSynchronization' => true]], ], - 'endtime' => [ + 'endtime' => [ 'exclude' => true, 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'eval' => 'datetime,int', - 'default' => 0, - 'range' => [ - 'upper' => mktime(0, 0, 0, 1, 1, 2038), - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], + 'config' => ['type' => 'datetime', 'default' => 0, 'range' => ['upper' => mktime(0, 0, 0, 1, 1, 2038)], 'behaviour' => ['allowLanguageSynchronization' => true]], ], - 'title' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_download.title', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_download.title.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'required' => true, - 'default' => '', - ], + // ----------------------------------------------------------- General + 'title' => [ + 'label' => 'Title', + 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'required' => true, 'default' => ''], ], - 'slug' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_download.slug', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_download.slug.description', - 'config' => [ + 'slug' => [ + 'label' => 'Slug', + 'config' => [ 'type' => 'slug', 'size' => 50, - 'generatorOptions' => [ - 'fields' => ['title'], // TODO: adjust this field to the one you want to use - 'fieldSeparator' => '-', - 'replacements' => [ - '/' => '', - ], - ], + 'generatorOptions' => ['fields' => ['title'], 'fieldSeparator' => '-', 'replacements' => ['/' => '']], 'fallbackCharacter' => '-', 'eval' => 'uniqueInPid', ], + ], + 'subtitle' => [ + 'label' => 'Subtitle', + 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''], + ], + // --------------------------------------------------------- List view + 'card_image' => [ + 'label' => 'Card Image', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], ], - 'singlepid' => [ - 'exclude' => false, - 'label' => 'Page ID of Success Story', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_download.title.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'required' => false, - 'default' => '', - ], + 'customer_logo' => [ + 'label' => 'Customer Logo', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], ], - 'teaser' => [ - 'exclude' => true, - 'label' => 'Short Teaser (optional)', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '', - ], + 'teaser' => [ + 'label' => 'Teaser', + 'config' => ['type' => 'text', 'cols' => 40, 'rows' => 4, 'eval' => 'trim'], ], - 'subtitle' => [ - 'exclude' => true, - 'label' => 'Subtitle', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '', - ], + + // ------------------------------------------------------ Detail · Hero + 'hero_bgimage' => [ + 'label' => 'Background Image', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], ], - 'hideonapp' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on App', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false, - ], - ], - ], + 'hero_small_image' => [ + 'label' => 'Small Image', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], ], - 'hideonwebsite' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Website', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false, - ], - ], - ], + 'hero_video' => [ + 'label' => 'Video', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'mp4,webm,ogv,mov,m4v'], ], - 'description' => [ - 'exclude' => true, - 'label' => 'Description', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '', - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]', + 'hero_overlay_color' => [ + 'label' => 'Overlay Color', + 'config' => ['type' => 'color'], ], - 'caseimage' => [ - 'exclude' => true, - 'label' => 'Case Image', - 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'sys_file_reference', - 'foreign_field' => 'uid_foreign', - 'foreign_sortby' => 'sorting_foreign', - 'foreign_table_field' => 'tablenames', - 'foreign_match_fields' => [ - 'fieldname' => 'caseimage', - ], - 'foreign_label' => 'uid_local', - 'foreign_selector' => 'uid_local', - 'overrideChildTca' => [ - 'columns' => [ - 'uid_local' => [ - 'config' => [ - 'appearance' => [ - 'elementBrowserType' => 'file', - 'elementBrowserAllowed' => 'jpg,jpeg,png,gif', - ], - ], - ], - ], - 'types' => [ - '0' => [ - 'showitem' => ' - --palette--;;imageoverlayPalette, - --palette--;;filePalette', - ], - ], - ], - 'maxitems' => 1, - 'appearance' => [ - 'headerThumbnail' => [ - 'field' => 'uid_local', - 'width' => '45', - 'height' => '45', - ], - 'enabledControls' => [ - 'info' => true, - 'new' => false, - 'dragdrop' => true, - 'sort' => false, - 'hide' => true, - 'delete' => true, - ], - 'fileUploadAllowed' => true, - ], - ], - ], - 'logoimage' => [ - 'exclude' => true, - 'label' => 'Customer Logo', - 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'sys_file_reference', - 'foreign_field' => 'uid_foreign', - 'foreign_sortby' => 'sorting_foreign', - 'foreign_table_field' => 'tablenames', - 'foreign_match_fields' => [ - 'fieldname' => 'logoimage', - ], - 'foreign_label' => 'uid_local', - 'foreign_selector' => 'uid_local', - 'overrideChildTca' => [ - 'columns' => [ - 'uid_local' => [ - 'config' => [ - 'appearance' => [ - 'elementBrowserType' => 'file', - 'elementBrowserAllowed' => 'jpg,jpeg,png,gif', - ], - ], - ], - ], - 'types' => [ - '0' => [ - 'showitem' => ' - --palette--;;imageoverlayPalette, - --palette--;;filePalette', - ], - ], - ], - 'maxitems' => 1, - 'appearance' => [ - 'headerThumbnail' => [ - 'field' => 'uid_local', - 'width' => '45', - 'height' => '45', - ], - 'enabledControls' => [ - 'info' => true, - 'new' => false, - 'dragdrop' => true, - 'sort' => false, - 'hide' => true, - 'delete' => true, - ], - 'fileUploadAllowed' => true, - ], - ], - ], - 'categories' => [ + 'hero_overlay_opacity' => [ + 'label' => 'Overlay Opacity (0–1)', 'config' => [ - 'type' => 'category' - ] + 'type' => 'number', + 'format' => 'decimal', + 'default' => 0, + 'range' => ['lower' => 0, 'upper' => 1], + 'slider' => ['step' => 0.05, 'width' => 200], + ], + ], + + // --------------------------------------------------- Detail · Content + 'content_elements' => [ + 'label' => 'Content Elements', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tt_content', + 'foreign_field' => 'tx_vitec_usecase_content', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'collapseAll' => true, + 'expandSingle' => true, + 'levelLinksPosition' => 'top', + 'showSynchronizationLink' => false, + 'showAllLocalizationLink' => false, + 'showPossibleLocalizationRecords' => false, + 'useSortable' => true, + ], + 'overrideChildTca' => [ + 'columns' => [ + 'colPos' => ['config' => ['default' => 999]], + ], + ], + ], + ], + + // --------------------------------------------------- Detail · Related + 'market' => [ + 'label' => 'Market', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'tx_vitec_domain_model_market', + 'items' => [['label' => '', 'value' => 0]], + 'default' => 0, + 'maxitems' => 1, + ], + ], + 'solutions' => [ + 'label' => 'Solutions', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_vitec_domain_model_solution', + 'MM' => 'tx_vitec_usecase_solution_mm', + 'size' => 6, + 'autoSizeMax' => 20, + 'maxitems' => 9999, + ], + ], + 'products' => [ + 'label' => 'Products', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_vitec_domain_model_product', + 'MM' => 'tx_vitec_usecase_product_mm', + 'size' => 6, + 'autoSizeMax' => 20, + 'maxitems' => 9999, + ], + ], + 'categories' => ['config' => ['type' => 'category']], + + // --------------------------------------------------------------- SEO + 'seo_title' => [ + 'label' => 'SEO Title', + 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim'], + ], + 'seo_description' => [ + 'label' => 'Meta Description', + 'config' => ['type' => 'text', 'cols' => 40, 'rows' => 3, 'eval' => 'trim'], + ], + 'no_index' => [ + 'label' => 'No Index', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]], + ], + 'no_follow' => [ + 'label' => 'No Follow', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]], + ], + 'canonical_link' => [ + 'label' => 'Canonical URL', + 'config' => ['type' => 'link'], + ], + 'og_title' => [ + 'label' => 'og:title', + 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim'], + ], + 'og_description' => [ + 'label' => 'og:description', + 'config' => ['type' => 'text', 'cols' => 40, 'rows' => 3, 'eval' => 'trim'], + ], + 'og_image' => [ + 'label' => 'og:image', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], + ], + 'twitter_title' => [ + 'label' => 'twitter:title', + 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim'], + ], + 'twitter_description' => [ + 'label' => 'twitter:description', + 'config' => ['type' => 'text', 'cols' => 40, 'rows' => 3, 'eval' => 'trim'], + ], + 'twitter_image' => [ + 'label' => 'twitter:image', + 'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'], + ], + + // -------------------------------------------------------- Appearance + 'layout_variant' => [ + 'label' => 'Detail Layout', + 'config' => [ + 'type' => 'select', 'renderType' => 'selectSingle', 'default' => 'standard', + 'items' => [ + ['label' => 'Standard', 'value' => 'standard'], + ['label' => 'Wide', 'value' => 'wide'], + ['label' => 'Centered', 'value' => 'centered'], + ['label' => 'Media-first', 'value' => 'media_first'], + ], + ], + ], + 'background_variant' => [ + 'label' => 'Background', + 'config' => [ + 'type' => 'select', 'renderType' => 'selectSingle', 'default' => 'none', + 'items' => [ + ['label' => 'None', 'value' => 'none'], + ['label' => 'Orange', 'value' => 'orange'], + ['label' => 'Blue', 'value' => 'blue'], + ['label' => 'Graphite', 'value' => 'graphite'], + ['label' => 'Midnight', 'value' => 'midnight'], + ['label' => 'Light', 'value' => 'light'], + ['label' => 'Dark', 'value' => 'dark'], + ], + ], + ], + 'accent_color' => [ + 'label' => 'Accent Color', + 'config' => ['type' => 'color'], + ], + 'hideonwebsite' => [ + 'exclude' => true, + 'label' => 'Hide on Website', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]], + ], + 'hideonapp' => [ + 'exclude' => true, + 'label' => 'Hide on App', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]], + ], + 'featured' => [ + 'label' => 'Featured', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]], + ], + 'show_related' => [ + 'label' => 'Show Related section', + 'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'default' => 1, 'items' => [['value' => '', 'label' => '']]], + ], + 'hero_layout' => [ + 'label' => 'Hero Layout', + 'config' => [ + 'type' => 'select', 'renderType' => 'selectSingle', 'default' => 'fullscreen', + 'items' => [ + ['label' => 'Fullscreen', 'value' => 'fullscreen'], + ['label' => 'Medium', 'value' => 'md'], + ['label' => 'Small', 'value' => 'sm'], + ], + ], + ], + 'text_theme' => [ + 'label' => 'Text Theme', + 'config' => [ + 'type' => 'select', 'renderType' => 'selectSingle', 'default' => 'light', + 'items' => [ + ['label' => 'Light', 'value' => 'light'], + ['label' => 'Dark', 'value' => 'dark'], + ], + ], ], ], -]; \ No newline at end of file +]; diff --git a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript b/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript old mode 100755 new mode 100644 index 9c9c56e..395e411 --- a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript +++ b/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript @@ -2,6 +2,12 @@ # VITEC container content elements — self-contained JSON rendering # Children collected via Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor # (exception-safe; returns [] on any error, never crashes the outer CE). +# +# Per-column flex settings (align/justify) live on the PARENT record +# (tx_vitec_col{1..4}_align/justify) and are attached by the processor to each +# column in `items` (items[].config.align/justify) — so they apply to ALL +# children of that column. The whole-grid gap (tx_vitec_gap) is emitted here as +# the parent-level `gap`. # ============================================================================= tt_content.vitec_cols_50_50 = JSON @@ -70,9 +76,27 @@ tt_content.vitec_cols_50_50 { header.field = header subheader = TEXT subheader.field = subheader + headerLayout = INT + headerLayout.field = header_layout + headerPosition = TEXT + headerPosition.field = header_position + headerLink = TEXT + headerLink { + htmlSpecialChars = 1 + typolink { + parameter.field = header_link + returnLast = result + } + } tx_vitec_bg_variant = TEXT tx_vitec_bg_variant.field = tx_vitec_bg_variant + # Parent-level: column gap scale (1–5) for the whole grid. + gap = TEXT + gap.field = tx_vitec_gap + + # Columns with their children + per-column flex (align/justify) in + # items[].config — assembled by ContainerChildrenProcessor. items = JSON items { dataProcessing { @@ -83,18 +107,48 @@ tt_content.vitec_cols_50_50 { } } -tt_content.vitec_cols_33_33_33 =< tt_content.vitec_cols_50_50 -tt_content.vitec_cols_25_25_25_25 =< tt_content.vitec_cols_50_50 -tt_content.vitec_cols_66_33 =< tt_content.vitec_cols_50_50 -tt_content.vitec_cols_33_66 =< tt_content.vitec_cols_50_50 +tt_content.vitec_cols_33_33_33 < tt_content.vitec_cols_50_50 +tt_content.vitec_cols_25_25_25_25 < tt_content.vitec_cols_50_50 +tt_content.vitec_cols_66_33 < tt_content.vitec_cols_50_50 +tt_content.vitec_cols_33_66 < tt_content.vitec_cols_50_50 # ----------------------------------------------------------------------------- # VITEC · Container — single-column container with a custom CSS class. # Inherits the full structure of the grid containers and adds the `cssClass` # field, read from the element's FlexForm (settings.cssClass). +# Single column -> no grid gap. # ----------------------------------------------------------------------------- -tt_content.vitec_container =< tt_content.vitec_cols_50_50 +tt_content.vitec_container < tt_content.vitec_cols_50_50 +tt_content.vitec_container.fields.gap > tt_content.vitec_container.fields.cssClass = TEXT tt_content.vitec_container.fields.cssClass.data = flexform:pi_flexform:settings.cssClass tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject = TEXT tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject.value = + +# ----------------------------------------------------------------------------- +# VITEC · Cards Carousel — container whose children (typically vitec_card +# elements) become the slides. Inherits the grid-container JSON structure +# (`items` via ContainerChildrenProcessor) and adds carousel settings read +# from the element's FlexForm. Single slide track -> no grid gap. +# ----------------------------------------------------------------------------- +tt_content.vitec_cards_carousel < tt_content.vitec_cols_50_50 +tt_content.vitec_cards_carousel.fields.gap > +tt_content.vitec_cards_carousel.fields.carousel = JSON +tt_content.vitec_cards_carousel.fields.carousel { + fields { + slidesPerView = TEXT + slidesPerView.data = flexform:pi_flexform:settings.slidesperview + showArrows = TEXT + showArrows.data = flexform:pi_flexform:settings.showarrows + showIndicators = TEXT + showIndicators.data = flexform:pi_flexform:settings.showindicators + loop = TEXT + loop.data = flexform:pi_flexform:settings.loop + autoplay = TEXT + autoplay.data = flexform:pi_flexform:settings.autoplay + autoplayInterval = TEXT + autoplayInterval.data = flexform:pi_flexform:settings.autoplayinterval + } +} +tt_content.vitec_cards_carousel.fields.cssClass = TEXT +tt_content.vitec_cards_carousel.fields.cssClass.data = flexform:pi_flexform:settings.cssClass diff --git a/packages/vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript b/packages/vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript old mode 100755 new mode 100644 diff --git a/packages/vitec/Configuration/TypoScript/Headless/vitec_news.typoscript b/packages/vitec/Configuration/TypoScript/Headless/vitec_news.typoscript new file mode 100644 index 0000000..943d917 --- /dev/null +++ b/packages/vitec/Configuration/TypoScript/Headless/vitec_news.typoscript @@ -0,0 +1,30 @@ +# ============================================================================= +# VITEC news integration — headless JSON output +# +# All supported EXT:news content element types use the same JSON renderer. +# The renderer returns a stable payload with: +# - mode = list | detail +# - items/news = serialised records +# - settings = the relevant FlexForm settings for the frontend +# ============================================================================= + +tt_content.news_pi1 =< lib.contentElementWithHeader +tt_content.news_pi1 { + fields { + content { + fields { + news = USER + news.userFunc = Evomedien\Vitec\UserFunc\NewsJsonRenderer->render + } + } + } +} + +tt_content.news_newsliststicky < tt_content.news_pi1 +tt_content.news_newsselectedlist < tt_content.news_pi1 +tt_content.news_newsdetail < tt_content.news_pi1 +tt_content.news_newsdatemenu < tt_content.news_pi1 +tt_content.news_categorylist < tt_content.news_pi1 +tt_content.news_newssearchform < tt_content.news_pi1 +tt_content.news_newssearchresult < tt_content.news_pi1 +tt_content.news_taglist < tt_content.news_pi1 \ No newline at end of file diff --git a/packages/vitec/Configuration/page.tsconfig b/packages/vitec/Configuration/page.tsconfig old mode 100755 new mode 100644 index 2191f64..b477a16 --- a/packages/vitec/Configuration/page.tsconfig +++ b/packages/vitec/Configuration/page.tsconfig @@ -80,6 +80,14 @@ mod { CType = vitec_datasheets } } + eventlist { + iconIdentifier = vitec-plugin-eventlist + title = VITEC Event List + description = Upcoming VITEC events (trade shows) with date, venue, booth and links + tt_content_defValues { + CType = vitec_eventlist + } + } simplecard { iconIdentifier = vitec-plugin-simplecard title = Simple Card @@ -97,6 +105,13 @@ mod { } } +tx_news.templateLayouts { + 1 = Compact List + 2 = Featured Story + 3 = Editorial Grid + 4 = Detail Spotlight +} + ############################################################ # Backend Previews für klassische Plugins (list_type) ############################################################ diff --git a/packages/vitec/ContentBlocks/Basics/ButtonStyle.yaml b/packages/vitec/ContentBlocks/Basics/ButtonStyle.yaml old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/Basics/SecondaryButtonStyle.yaml b/packages/vitec/ContentBlocks/Basics/SecondaryButtonStyle.yaml old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/card/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/card/assets/icon.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/card/config.yaml b/packages/vitec/ContentBlocks/ContentElements/card/config.yaml old mode 100755 new mode 100644 index f70852f..f275f16 --- a/packages/vitec/ContentBlocks/ContentElements/card/config.yaml +++ b/packages/vitec/ContentBlocks/ContentElements/card/config.yaml @@ -13,12 +13,27 @@ fields: type: Text max: 50 - - identifier: header - useExistingField: true - required: true + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true - - identifier: subheader - useExistingField: true - identifier: bodytext useExistingField: true diff --git a/packages/vitec/ContentBlocks/ContentElements/card/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/card/language/labels.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html old mode 100755 new mode 100644 index 576085a..7160094 --- a/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html @@ -50,7 +50,7 @@ {data.header} - ⚠ Headline fehlt + ⚠ Headline missing diff --git a/packages/vitec/ContentBlocks/ContentElements/card/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/card/templates/frontend.html old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/columns/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/columns/assets/icon.svg new file mode 100644 index 0000000..46aa6f0 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/columns/assets/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/columns/config.yaml b/packages/vitec/ContentBlocks/ContentElements/columns/config.yaml new file mode 100644 index 0000000..dfc6682 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/columns/config.yaml @@ -0,0 +1,91 @@ +name: vitec/columns +group: vitec_custom_components +prefixFields: true +prefixType: vendor + +fields: + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true + - identifier: layout + type: Select + renderType: selectSingle + default: cols_50_50 + items: + - label: '50 / 50' + value: cols_50_50 + - label: '66 / 33' + value: cols_66_33 + - label: '33 / 66' + value: cols_33_66 + + - identifier: items + type: Collection + label: Items + labelField: headline + fields: + - identifier: column + type: Select + renderType: selectSingle + default: left + items: + - label: Left column + value: left + - label: Right column + value: right + - identifier: item_type + type: Select + renderType: selectSingle + default: text + items: + - label: Text + value: text + - label: Image + value: image + - label: Video + value: video + - label: CTA / Button + value: cta + - identifier: headline + type: Text + max: 255 + - identifier: text + type: Textarea + enableRichtext: true + - identifier: image + type: File + minitems: 0 + maxitems: 1 + allowed: common-image-types + - identifier: video + type: File + minitems: 0 + maxitems: 1 + allowed: mp4,webm,ogv,mov,m4v + - identifier: link + type: Link + allowedTypes: + - page + - url + - file + - email + - identifier: link_label + type: Text + max: 60 diff --git a/packages/vitec/ContentBlocks/ContentElements/columns/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/columns/language/labels.xlf new file mode 100644 index 0000000..1b4ca10 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/columns/language/labels.xlf @@ -0,0 +1,45 @@ + + + + + + VITEC · Columns + + + Two-column layout (50/50, 66/33, 33/66). Add items and assign each to the left or right column. + + + + Layout + + + + Items + + + Column + + + Type + + + Headline + + + Text + + + Image + + + Video + + + Link + + + Link Label + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/columns/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/columns/templates/backend-preview.html new file mode 100644 index 0000000..eae1d0a --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/columns/templates/backend-preview.html @@ -0,0 +1,19 @@ + + + + + + +
+
+
+
VITEC · Columns
+

Two-column layout — {data.layout}

+
+
+ {data.layout} +
+
+ +
+ diff --git a/packages/vitec/ContentBlocks/ContentElements/columns/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/columns/templates/frontend.html new file mode 100644 index 0000000..4b9267a --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/columns/templates/frontend.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/cta-banner/config.yaml b/packages/vitec/ContentBlocks/ContentElements/cta-banner/config.yaml old mode 100755 new mode 100644 index 5129152..79e8354 --- a/packages/vitec/ContentBlocks/ContentElements/cta-banner/config.yaml +++ b/packages/vitec/ContentBlocks/ContentElements/cta-banner/config.yaml @@ -4,9 +4,26 @@ prefixFields: true prefixType: vendor fields: - - identifier: header - useExistingField: true - required: true + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true - identifier: bodytext useExistingField: true diff --git a/packages/vitec/ContentBlocks/ContentElements/cta-banner/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/cta-banner/language/labels.xlf index e49f186..f7823c7 100644 --- a/packages/vitec/ContentBlocks/ContentElements/cta-banner/language/labels.xlf +++ b/packages/vitec/ContentBlocks/ContentElements/cta-banner/language/labels.xlf @@ -6,7 +6,7 @@ VITEC · CTA Banner - Call-to-Action Banner mit optionalem zweiten Button und Background-Varianten + Call-to-action banner with optional secondary button and background variants @@ -35,7 +35,7 @@ Background Image (optional) - Optionales Hintergrundbild — überlagert die Background-Variant-Farbe + Optional background image — overlays the background variant colour diff --git a/packages/vitec/ContentBlocks/ContentElements/cta-banner/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/cta-banner/templates/backend-preview.html index d5b2c74..228bc8b 100644 --- a/packages/vitec/ContentBlocks/ContentElements/cta-banner/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/cta-banner/templates/backend-preview.html @@ -6,7 +6,7 @@
- Thumbnail (Background Image oder Farb-Placeholder) + Thumbnail (background image or colour placeholder) {data.header} - ⚠ Headline fehlt + ⚠ Headline missing @@ -75,7 +75,7 @@ - Layout Variant Badge mit Icon + Layout variant badge with icon ◎ Centered @@ -85,9 +85,9 @@ - Warnung wenn Primary-CTA-Link fehlt + Warning when primary CTA link is missing - ⚠ Kein Primary-Link + ⚠ Primary link missing
diff --git a/packages/vitec/ContentBlocks/ContentElements/faq/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/faq/assets/icon.svg new file mode 100644 index 0000000..fd78199 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/faq/assets/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/faq/config.yaml b/packages/vitec/ContentBlocks/ContentElements/faq/config.yaml new file mode 100644 index 0000000..663d36d --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/faq/config.yaml @@ -0,0 +1,56 @@ +name: vitec/faq +group: vitec_custom_components +prefixFields: true +prefixType: vendor + +fields: + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true + + + + + + - identifier: faq_items + type: Collection + labelField: question + minitems: 1 + fields: + - identifier: question + type: Text + required: true + max: 255 + + - identifier: answer + type: Textarea + enableRichtext: true + + - identifier: first_item_open + type: Checkbox + default: 1 + + - identifier: allow_multiple_open + type: Checkbox + default: 0 + + - identifier: debug + type: Checkbox + default: 0 diff --git a/packages/vitec/ContentBlocks/ContentElements/faq/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/faq/language/labels.xlf new file mode 100644 index 0000000..5034ab9 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/faq/language/labels.xlf @@ -0,0 +1,33 @@ + + + + + + VITEC · FAQ + + + Accordion with repeatable question/answer items. First item can start expanded. + + + + FAQ Entries + + + Question + + + Answer + + + + Open first item by default + + + Allow multiple items open at once + + + Allow Debug Output + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/faq/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/faq/templates/backend-preview.html new file mode 100644 index 0000000..e7f7dce --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/faq/templates/backend-preview.html @@ -0,0 +1,66 @@ + + + + + + +
+ +
?
+ +
+
VITEC · FAQ
+ +

+ + {data.header} + + ⚠ Headline missing + + +

+ + +
{data.subheader}
+
+ + + +
+ + ▸ {item.question}
+
+
+
+ +
+ ⚠ No FAQ entries +
+
+
+
+ +
+ + + ❓ entries + + + + ▽ First item open + + + + ≡ Multiple open allowed + + + + ⚙ Debug ON + + +
+ +
+ +
+ diff --git a/packages/vitec/ContentBlocks/ContentElements/faq/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/faq/templates/frontend.html new file mode 100644 index 0000000..da9c5f0 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/faq/templates/frontend.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/hero-section/config.yaml b/packages/vitec/ContentBlocks/ContentElements/hero-section/config.yaml old mode 100755 new mode 100644 index 0a3d732..27c3dca --- a/packages/vitec/ContentBlocks/ContentElements/hero-section/config.yaml +++ b/packages/vitec/ContentBlocks/ContentElements/hero-section/config.yaml @@ -8,21 +8,30 @@ fields: type: Text max: 50 - - identifier: header - useExistingField: true - required: true + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true - - identifier: header_layout - useExistingField: true - - identifier: header_position - useExistingField: true - - identifier: header_link - useExistingField: true - - identifier: subheader - useExistingField: true - identifier: bodytext useExistingField: true diff --git a/packages/vitec/ContentBlocks/ContentElements/hero-section/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/hero-section/language/labels.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/hero-section/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/hero-section/templates/backend-preview.html index 1c1c794..ce1e316 100644 --- a/packages/vitec/ContentBlocks/ContentElements/hero-section/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/hero-section/templates/backend-preview.html @@ -31,7 +31,7 @@ {data.header} - ⚠ Headline fehlt + ⚠ Headline missing @@ -53,7 +53,7 @@ - Settings Sidebar (ohne Partial) + Settings sidebar (without partial)
@@ -65,7 +65,7 @@ - ⚠ Kein Bild + ⚠ No image
diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/assets/icon.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/config.yaml b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/config.yaml old mode 100755 new mode 100644 index dc82845..8614a56 --- a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/config.yaml +++ b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/config.yaml @@ -4,21 +4,30 @@ prefixFields: true prefixType: vendor fields: - - identifier: header - useExistingField: true - required: true + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true - - identifier: header_layout - useExistingField: true - - identifier: header_position - useExistingField: true - - identifier: header_link - useExistingField: true - - identifier: subheader - useExistingField: true - identifier: bodytext useExistingField: true diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/language/labels.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html old mode 100755 new mode 100644 index b8e5138..7c0cdcb --- a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html @@ -12,7 +12,7 @@ {data.header} - ⚠ Headline fehlt + ⚠ Headline missing @@ -61,7 +61,7 @@ - ⚠ Button-Text ohne Link + ⚠ Button text without link diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/frontend.html old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/video/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/video/assets/icon.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/video/config.yaml b/packages/vitec/ContentBlocks/ContentElements/video/config.yaml old mode 100755 new mode 100644 index 8a033b8..4f6e6dd --- a/packages/vitec/ContentBlocks/ContentElements/video/config.yaml +++ b/packages/vitec/ContentBlocks/ContentElements/video/config.yaml @@ -4,21 +4,30 @@ prefixFields: true prefixType: vendor fields: - - identifier: header - useExistingField: true - required: true + - identifier: header_section + type: Palette + label: Header + fields: + - identifier: header + useExistingField: true + required: true + - type: Linebreak + - identifier: header_layout + useExistingField: true + - identifier: header_position + useExistingField: true + - identifier: date + useExistingField: true + - type: Linebreak + - identifier: header_link + useExistingField: true + - type: Linebreak + - identifier: subheader + useExistingField: true - - identifier: header_layout - useExistingField: true - - identifier: header_position - useExistingField: true - - identifier: header_link - useExistingField: true - - identifier: subheader - useExistingField: true - identifier: youtube_code type: Text diff --git a/packages/vitec/ContentBlocks/ContentElements/video/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/video/language/labels.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/ContentBlocks/ContentElements/video/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/video/templates/backend-preview.html old mode 100755 new mode 100644 index 04cd7da..5ab3bad --- a/packages/vitec/ContentBlocks/ContentElements/video/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/video/templates/backend-preview.html @@ -27,7 +27,7 @@ {data.header} - ⚠ Headline fehlt + ⚠ Headline missing @@ -59,7 +59,7 @@ - ⚠ Kein Video-Source + ⚠ No video source ⏸ Controls hidden diff --git a/packages/vitec/ContentBlocks/ContentElements/video/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/video/templates/frontend.html old mode 100755 new mode 100644 diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md new file mode 100644 index 0000000..b885358 --- /dev/null +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -0,0 +1,619 @@ +# VITEC Headless JSON Architecture — Software Architecture and Interface Specification + +| | | +|---|---| +| **Document identifier** | EVO‑VITEC‑HL‑001 | +| **Version** | 1.0 | +| **Status** | Released | +| **Date** | 2026‑07‑09 | +| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | +| **Owner** | evomedien — VITEC relaunch | + +This document is drafted in the style of, and adopts the terminology conventions of, +ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for +users) and ISO/IEC 25010 (product quality). The key words **shall**, **should** and +**may** are to be interpreted as normative requirements, recommendations and +permissions respectively. + +--- + +## Foreword + +The VITEC web platform is a *headless* TYPO3 installation: the CMS does not render +HTML pages, it emits JSON that is consumed by a separate React front end. This +specification describes the architecture, the public JSON interface, and the +engineering conventions that keep the JSON output **consistent** across all content +types and **maintainable** across TYPO3 and extension upgrades. + +It supersedes, as the authoritative reference, the informal tutorial +`Documentation/HeadlessIntegration.md`, which is retained as an informative how‑to. + +## Introduction + +The platform combines the generic headless page renderer (`friendsoftypo3/headless`) +with three sources of content JSON: + +1. **Custom plugins** (product, use case/success story, market, solution, downloads, + datasheets, events, news) rendered by dedicated *UserFunc* classes; +2. **Layout containers** (b13/container based column grids and a card carousel) + rendered by a *DataProcessor*; +3. **Content Blocks** (`friendsoftypo3/content-blocks`) serialised automatically by + `nb-headless-content-blocks`. + +All three are unified under a single **content‑element envelope** so that the front +end can consume every element with one predictable shape. + +--- + +## 1 Scope + +### 1.1 In scope +This document specifies: +- the runtime environment and the software stack (Clause 5); +- the JSON rendering pipeline and its layers (Clause 6); +- the public JSON interface — envelope, payloads, structured data (Clause 7); +- the catalogue of content types and their JSON keys (Clause 8); +- the mandatory conventions for implementing and extending renderers (Clause 9); +- maintainability and upgrade‑safety requirements (Clause 10); +- conformance criteria (Clause 11). + +### 1.2 Out of scope +Front‑end (React) implementation, hosting/deployment, the editorial (backend) TCA +form design except where it determines JSON output, and non‑headless (Fluid) rendering +paths. + +## 2 Normative references + +The following documents are referred to in the text. For dated references, only the +edition cited applies. + +- ISO/IEC/IEEE 42010, *Software, systems and enterprise — Architecture description* +- ISO/IEC 25010, *Systems and software Quality Requirements and Evaluation (SQuaRE) — Product quality model* +- ISO/IEC/IEEE 26514, *Systems and software engineering — Design and development of information for users* +- ISO 8601‑1, *Date and time — Representations for information interchange* +- IETF RFC 8259, *The JavaScript Object Notation (JSON) Data Interchange Format* +- IETF RFC 2119, *Key words for use in RFCs to indicate requirement levels* +- schema.org vocabulary (informative), *https://schema.org* + +## 3 Terms and definitions + +**3.1 headless** — operating mode in which TYPO3 returns JSON instead of HTML; +enabled per site by `headless: 1` and the headless Site Sets. + +**3.2 content element** — a `tt_content` record; the atomic unit of page content. + +**3.3 CType** — the content‑element type identifier stored in `tt_content.CType` +(e.g. `vitec_productlist`, `news_pi1`, `vitec_cols_50_50`). + +**3.4 envelope** — the invariant outer JSON structure shared by every content element +(Clause 7.2). + +**3.5 payload** — the domain‑specific JSON produced for one content element and placed +inside the envelope (Clause 7.3). + +**3.6 renderer** — a *UserFunc* class under `Classes/UserFunc/` that produces a payload. + +**3.7 serializer** — a service class under `Classes/Service/` that converts a domain +record into JSON, used by one or more renderers. + +**3.8 resolver / processor** — `ContentElementResolver` and `ContainerChildrenProcessor`; +they normalise a raw `tt_content` row into an envelope and resolve nested elements. + +**3.9 container** — a b13/container CType that owns child content elements via +`tx_container_parent` and emits them as `items`. + +**3.10 Content Block** — a declaratively defined content element +(`friendsoftypo3/content-blocks`), serialised by `nb-headless-content-blocks`. + +## 4 Symbols and abbreviated terms + +| Term | Meaning | +|---|---| +| FAL | File Abstraction Layer (TYPO3 file handling) | +| IRRE | Inline Relational Record Editing (`type: inline`) | +| MM | Many‑to‑many junction table | +| CB | Content Block | +| TS | TypoScript | +| CE | Content element | + +--- + +## 5 Runtime environment (architecture context) + +### 5.1 Software stack +| Component | Version | Role | +|---|---|---| +| TYPO3 CMS | ^14.3 | Core CMS | +| PHP | 8.x (per TYPO3 14) | Runtime | +| `friendsoftypo3/headless` | ^5.0@rc | Page‑to‑JSON renderer, `lib.contentElement` | +| `friendsoftypo3/content-blocks` | ^2.4 | Declarative content elements | +| `netzbewegung/nb-headless-content-blocks` | ^0.0.23 | Content Blocks → JSON | +| `b13/container` | ^3.1 | Nested column containers | +| `georgringer/news` | ^14.0 | News records and plugins | +| `evomedien/vitec` | ^1.0 | This project’s custom extension | + +> **NOTE** `friendsoftypo3/headless` is pinned to a **release candidate** (`^5.0@rc`). +> This is an upgrade‑sensitivity point; see 10.4. + +### 5.2 Site configuration +The headless mode is activated in `config/sites/vitec/config.yaml`: + +```yaml +base: / +headless: 1 +frontendBase: '' +dependencies: + - friendsoftypo3/headless + - friendsoftypo3/headless-mixed + - nb-headless-content-blocks/headless-content-blocks + - georgringer/news +``` + +The Site **Sets** listed under `dependencies` load, in order, the headless TypoScript +base, the mixed‑mode overrides, the Content Blocks JSON integration and the News +integration. The VITEC Set (`EXT:vitec/Configuration/Sets/Vitecset`) layers the custom +definitions on top. The headless page response carries +`Content-Type: application/json; charset=utf-8`. Slug routing is configured with route +enhancers for products (`tx_vitec_domain_model_product.slug`) and news detail +(`path_segment`). + +### 5.3 Architectural principles (rationale) +- **P1 — One envelope.** Every content element, regardless of source, is exposed with + the same outer shape so the front end has a single rendering contract. +- **P2 — Payload isolation.** Domain JSON is produced in PHP, fully decoupled from + TypoScript, so business logic is testable and versionable. +- **P3 — Dual entry.** Each renderer works both as a top‑level plugin and as a nested + child of a container (Clause 9.3). +- **P4 — Fail soft.** A failing element yields empty output, never a broken page + (Clause 9.5). + +--- + +## 6 Rendering pipeline + +The JSON for one page is assembled top‑down through the following layers. + +``` +HTTP request (Accept: application/json, headless:1) + │ + ▼ +[L1] Page renderer friendsoftypo3/headless + │ builds { meta, content[], … , jsonLd } + ▼ +[L2] Content‑element envelope lib.contentElement / lib.contentElementWithHeader + │ per CType: id, type, colPos, appearance, content{header,…} + ▼ +[L3] Payload injection USER cObj → Classes/UserFunc/*JsonRenderer::render + │ places domain JSON under content. + ▼ +[L4] Containers tt_content.vitec_cols_* = JSON + │ ContainerChildrenProcessor → items[].contentElements[] + ▼ +[L5] Normalisation & nesting ContentElementResolver / PLUGIN_RENDERERS + ▼ +[L6] Structured data (JSON‑LD) PageJsonLdRenderer + StructuredDataService +``` + +### 6.1 L1 — Page renderer +`friendsoftypo3/headless` converts the requested page into a JSON document containing +page metadata, the ordered array of content elements, navigation and the JSON‑LD +graph. VITEC does not replace this layer; it contributes elements to `content[]` +(L2–L5) and the `jsonLd` field (L6). + +### 6.2 L2 — Content‑element envelope +Each CType is bound to a headless library object: + +```typoscript +tt_content. < lib.contentElementWithHeader +``` + +`lib.contentElement` provides `id`, `type`, `colPos`, `categories`, `appearance`. +`lib.contentElementWithHeader` additionally provides, under `content`, the standard +header fields: `header`, `subheader`, `headerLayout`, `headerPosition`, `headerLink` +(link resolved via typolink). **All VITEC plugins and all News CTypes inherit +`lib.contentElementWithHeader`**, giving a uniform header section in both backend and +JSON (see the companion header convention). + +### 6.3 L3 — Payload injection +The domain payload is added as a `USER` content object under `content.fields.`: + +```typoscript +tt_content.vitec_productlist < lib.contentElementWithHeader +tt_content.vitec_productlist { + fields { + content { + fields { + products = USER + products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render + } + } + } +} +``` + +`render()` returns a JSON string that the headless JSON cObject embeds verbatim at +`content.products`. The key is **plural for list plugins** and **singular for detail +plugins** (Clause 9.6). + +### 6.4 L4 — Containers +Column containers are defined as a self‑contained JSON object, not via +`lib.contentElement`: + +```typoscript +tt_content.vitec_cols_50_50 = JSON +tt_content.vitec_cols_50_50.fields { + id … type … appearance … header … subheader … + gap = TEXT # tx_vitec_gap (whole‑grid gap) + items = JSON + items.dataProcessing.10 = Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor +} +tt_content.vitec_cols_33_66 < tt_content.vitec_cols_50_50 +tt_content.vitec_container < tt_content.vitec_cols_50_50 # single column, no gap +tt_content.vitec_cards_carousel < tt_content.vitec_cols_50_50 # + carousel settings +``` + +> **RULE (normative)** Container CTypes **shall** be derived with the **copy** +> operator `<`, never the reference operator `=<`. A `tt_content → tt_content` +> reference is not recognised as an independent renderer by the headless content +> mapper and silently falls back to raw output (see 10.5, and Annex B‑1). + +### 6.5 L5 — Normalisation and nested plugins +`ContainerChildrenProcessor` queries children by `tx_container_parent`, groups them by +`colPos` and emits, per column, a flex configuration plus the resolved children. Each +child is normalised by the same envelope logic as `ContentElementResolver`. When a +child is itself a VITEC plugin, it is resolved through the shared `PLUGIN_RENDERERS` +map (Clause 7.4). + +### 6.6 L6 — Structured data (JSON‑LD) +`PageJsonLdRenderer` (bound at `page…fields.jsonLd`) assembles a schema.org `@graph` +via `StructuredDataService`, encoded with `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. +Node types and their triggers: + +| Node | Emitted when | +|---|---| +| `Organization` | every page (from site settings `seo.organization.*`) | +| `WebSite` | only on the site root page | +| `BreadcrumbList` | from the rootline (spacer/folder/recycler doktypes skipped) | +| `NewsArticle` | on news‑detail page layouts (`pages.layout ∈ {13,14,15}`) | +| `FAQPage` | when the page contains `vitec_faq` elements → `vitec_faq_items` collection | +| `ExhibitionEvent` | from `vitec_eventlist` elements → `tx_vitec_domain_model_event` (windowed by the FlexForm `daysinadvance`) | +| `Product`, `VideoObject` | via `ProductShowJsonRenderer` for product detail pages | + +FAQ and event nodes are collected from the respective tables; event image URLs are +currently built with a hard‑coded `/fileadmin` prefix (see 10.3 / Annex B‑5). + +--- + +## 7 JSON interface specification + +### 7.1 Encoding +Output **shall** be RFC 8259 JSON, UTF‑8. Timestamps **shall** be Unix epoch seconds +(integer); where ISO 8601 strings are required by schema.org they are produced inside +the JSON‑LD layer. + +### 7.2 Content‑element envelope +Every element in `content[]` conforms to: + +```jsonc +{ + "id": 123, // tt_content.uid + "type": "vitec_productlist", // tt_content.CType + "colPos": 0, + "appearance": { + "layout": "0", + "frameClass": "default", + "spaceBefore": "", + "spaceAfter": "" + }, + "content": { // present for lib.contentElement(WithHeader) CTypes + "header": "…", "subheader": "…", + "headerLayout": 2, "headerPosition": "", "headerLink": "https://…", + "": { /* payload, Clause 7.3 */ } + } +} +``` + +Elements produced by the resolver/processor (container children, inline story CEs) use +a lean variant of the envelope: + +```jsonc +{ "id": 456, "type": "text", "colPos": 211, "sorting": 1, + "appearance": { … }, "data": { /* non‑system fields */ } } +``` + +### 7.3 Payload keys +| Kind | Key | Cardinality | +|---|---|---| +| List plugin | plural noun (`products`, `usecases`, `news → items`) | array | +| Detail plugin | singular noun (`product`, `usecase`, `market`, `solution`) | object | +| Container | `items` | array of `{config, contentElements}` | + +### 7.4 Container payload +```jsonc +{ + "type": "vitec_cols_33_66", + "header": "…", "headerLayout": 2, "headerLink": "…", + "gap": "3", + "items": [ + { "config": { "colPos": 251, "align": "stretch", "justify": "flex-start" }, + "contentElements": [ /* normalised children, recursively */ ] }, + { "config": { "colPos": 252, "align": "center", "justify": "space-between" }, + "contentElements": [ … ] } + ] +} +``` + +- `gap` is a **parent‑level** property (whole‑grid gap). +- `align`/`justify` are **per‑column** and are read from the parent record + (`tx_vitec_col{N}_align/justify`); they therefore apply to **all** children of that + column. These container‑only fields **shall not** appear in a child’s `data` + (enforced by `CONTAINER_FIELDS` filtering). + +### 7.5 Success Story (use case) detail payload +Produced by `UsecaseSerializer::serializeDetail()` — the reference implementation of +the centralised pattern (Clause 9.2): + +```jsonc +{ + "uid": 1, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…", + "cardImage": { "url": "…", "srcset": [ … ] }, "customerLogo": { … }, + "hero": { "bgImage": …, "smallImage": …, "video": …, + "overlayColor": "#000", "overlayOpacity": 0.4, + "layout": "fullscreen", "textTheme": "light" }, + "contentElements": [ /* inline CEs; the vitec_columns block is resolved specially */ ], + "related": { "show": true, "market": {…}, "solutions": [ … ], "products": [ … ], + "categories": [ … ] }, + "seo": { "title": "…", "description": "…", "canonical": "…", + "robots": { "noIndex": false, "noFollow": false }, + "openGraph": { "title": …, "description": …, "image": … }, + "twitter": { "title": …, "description": …, "image": … } }, + "appearance": { "layoutVariant": "standard", "backgroundVariant": "none", + "accentColor": "", "featured": false, … } +} +``` + +SEO fields use fallback resolution (`seo_* → title/teaser`; `og_* → seo_* → title`; +`twitter_* → og_*`; images `og_image → card_image → hero_bgimage`) so the front end +always receives complete metadata. + +### 7.6 News payload +Produced by `NewsJsonRenderer` under `content.news`: + +```jsonc +{ "mode": "list", // "list" | "detail" | "error" + "items": [ { "uid": 1, "title": "…", "alternativeTitle": "…", + "pathSegment": "…", "detailUrl": "/news/…", + "canonicalUrl": "https://…", "teaser": "…", "bodytext": "…", + "datetime": 1625097600, "categories": [ … ], "media": [ … ] } ], + "settings": { "type": "news_pi1", "templateLayout": "1", + "templateLayoutLabel": "Compact List", "detailPid": 45, … } } +``` + +Ten News CTypes (`news_pi1`, `news_newsdetail`, `news_newsliststicky`, …) share one +renderer; `news_newsdetail` yields `mode: "detail"` with a single `news` object. + +### 7.7 Content Blocks +Content Blocks (`card`, `cta-banner`, `hero-section`, `intro-paragraph`, `video`, +`faq`, `columns`) are serialised automatically by `nb-headless-content-blocks`: its +`ContentBlocksJsonDataProcessor` converts the resolved record via `RecordToArray`, +dropping system fields and recursing into files (→ URL + metadata), collections +(e.g. `faq_items` → array of child records) and typolinks. The output uses the lean +envelope (`{id, type, colPos, sorting, appearance, data}`). An optional per‑block +`headless.php` hook may transform the array (none are currently defined). Fields carry +the `vitec_` vendor prefix in storage. The **`vitec_columns`** block is +the single exception: because it is authored **inline inside a Success Story record**, +its collection items are resolved explicitly by `UsecaseSerializer::resolveColumnsElement()` +into `{ header…, layout, columns: { left: [], right: [] } }`, with the collection +storage table discovered from TCA (`foreign_table`) rather than hard‑coded. + +--- + +## 8 Content‑type catalogue + +| CType | TS pattern | Renderer / Processor | Payload key | Kind | +|---|---|---|---|---| +| `vitec_productlist` | `< lib.contentElementWithHeader` | ProductListJsonRenderer | `products` | list | +| `vitec_productshow` | `< lib.…WithHeader` | ProductShowJsonRenderer | `product` | detail | +| `vitec_usecaselist` | `< lib.…WithHeader` | UsecaseListJsonRenderer → **UsecaseSerializer** | `usecases` | list | +| `vitec_usecaseshow` | `< lib.…WithHeader` | UsecaseShowJsonRenderer → **UsecaseSerializer** | `usecase` | detail | +| `vitec_marketshow` | `< lib.…WithHeader` | MarketShowJsonRenderer | `market` | detail | +| `vitec_solutionshow` | `< lib.…WithHeader` | SolutionShowJsonRenderer | `solution` | detail | +| `vitec_downloadcard` | `< lib.…WithHeader` | DownloadcardJsonRenderer | `downloadcard` | detail | +| `vitec_downloadcardcollection` | `< lib.…WithHeader` | DownloadcardcollectionJsonRenderer | `downloadcardcollection` | list | +| `vitec_datasheets` | `< lib.…WithHeader` | DatasheetsJsonRenderer | `datasheets` | list | +| `vitec_eventlist` | `=< lib.…WithHeader` ⚠ | EventlistJsonRenderer | `eventlist` | list | +| `news_pi1` (+9 variants) | `=< lib.…WithHeader`; variants `<` | NewsJsonRenderer | `news` | list/detail | +| `vitec_cols_50_50` | `= JSON` | ContainerChildrenProcessor | `items` | container | +| `vitec_cols_33_66 / 66_33 / 33_33_33 / 25_25_25_25` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` | container | +| `vitec_container` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` | container (1 col) | +| `vitec_cards_carousel` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` + `carousel` | container | +| Content Blocks (`vitec_card`, …) | Content Blocks + nb‑headless | — | (auto) | element | +| `vitec_columns` (CB, inline) | Content Blocks | UsecaseSerializer (special) | `columns` | element | + +⚠ = uses the reference operator `=<`; see Annex B‑1. + +--- + +## 9 Conventions (normative) + +These rules define the **single, uniform way** to implement and extend headless +renderers. New code **shall** comply; existing code **should** be aligned when touched. + +### 9.1 Renderer class shape +A payload renderer **shall**: +1. reside in `Classes/UserFunc/` and be named `JsonRenderer`; +2. expose `#[AsAllowedCallable] public function render(string $content, array $conf): string`; +3. expose `public function renderForRecord(array $row): string` for reuse by + containers/resolvers (Clause 9.3); +4. return a JSON **string** (never an array/object). + +### 9.2 Serialisation ownership +Domain‑to‑JSON conversion **should** live in a `Classes/Service/*Serializer` class, and +the renderer **should** be a thin wrapper around it. `UsecaseSerializer` is the +reference implementation. New list/detail pairs **shall** share one serializer. + +### 9.3 Dual‑entry pattern +`render()` **shall** handle top‑level invocation: use `$this->cObj->data` when it is the +plugin’s own row, otherwise perform page discovery (Clause 9.4). `renderForRecord()` +**shall** accept an explicit `tt_content` row and be free of page/context assumptions, +so it can be called by `ContainerChildrenProcessor` and `ContentElementResolver`. + +### 9.4 Page‑id discovery +Renderers **shall** resolve the current page id in this order: +```php +$id = $GLOBALS['TYPO3_REQUEST']?->getAttribute('frontend.page.information')?->getId() ?? 0; +if ($id <= 0) { $id = (int)($GLOBALS['TSFE']->id ?? 0); } // fallback +``` +Reliance on `$GLOBALS['TSFE']->id` alone is **prohibited** (it is frequently `null` in +the JSON cObject context). + +### 9.5 Fail‑soft error handling +The body of `renderForRecord()` **shall** be wrapped in +`try { … } catch (\Throwable $e) { return ''; }`. Diagnostic output **may** be emitted +only when a FlexForm `debug` flag is set. A failing element **shall not** propagate an +exception to the page. + +### 9.6 Naming +- List payload keys **shall** be plural; detail keys **shall** be singular. +- JSON property names **shall** be `camelCase` for computed/composed fields; raw + passthrough fields in `data` retain their database names. + +### 9.7 Nested‑plugin registration +A plugin that may appear inside a container **shall** be registered in the +`PLUGIN_RENDERERS` map in **both** `ContentElementResolver` and +`ContainerChildrenProcessor`, as `CType => [RendererClass::class, 'jsonKey']`. + +> **NOTE** The duplicated map is a known maintenance hazard (10.3). Until it is +> centralised, both copies **shall** be kept in sync. + +### 9.8 Header section +Every custom CE/plugin/container **shall** expose the standard header section (the core +`headers` palette; Content Blocks use a `header_section` palette). See the companion +"header convention". Header fields in JSON use the names `header`, `subheader`, +`headerLayout`, `headerPosition`, `headerLink`. + +--- + +## 10 Maintainability and upgrade‑safety (ISO 25010) + +This clause records the quality characteristics *maintainability* and *portability* +and the concrete risks and rules that preserve them. + +### 10.1 Modularity — current state +Strengths: uniform envelope, dual‑entry pattern, exception safety, a shared +`PLUGIN_RENDERERS` map, and the `UsecaseSerializer` reference pattern. +Weakness: **substantial duplication** across the inline renderers. + +### 10.2 Reusability — duplication register +The following logic is duplicated across many renderers and **should** be extracted +into shared services (target design in parentheses): + +| Duplicated logic | Occurrences | Target service | +|---|---|---| +| FAL image/`srcset` resolution | Product(List/Show), Market, Solution, Event, Datasheets | `FalImageResolver` | +| FAL video resolution | Product, Usecase, hero | `FalImageResolver::video()` | +| `sys_category` MM query | ≥ 9 renderers | `CategoryResolver` | +| Custom MM (product↔download, …) | ≥ 5 renderers | `RelationResolver::resolveMany()` | +| Download file‑by‑convention | 5 renderers | `ConventionFileResolver` | +| `letterSequenceToRank()` sort helper | 5 renderers | static utility | +| Page‑id discovery | all renderers | `PageIdResolver::resolve()` | + +> **RULE** When a shared resolver service exists, new renderers **shall** use it and +> **shall not** re‑implement the logic inline. + +### 10.3 Analysability — single sources of truth +- The `PLUGIN_RENDERERS` map exists in two files (10.2/9.7); it **should** be promoted + to one shared constant/class. +- Table and field names are string literals scattered across renderers. New code + **shall** define table/field names as **class constants** (as `UsecaseSerializer` + and the processors already do) to localise upgrade impact. + +### 10.4 Portability — upgrade‑sensitivity points +| Point | Risk | Mitigation | +|---|---|---| +| `friendsoftypo3/headless ^5.0@rc` | RC; `lib.contentElement(WithHeader)` shape may change | Pin exact RC; re‑verify envelope after any bump; keep payloads decoupled (P2) | +| `nb-headless-content-blocks ^0.0.x` | pre‑1.0; CB→JSON shape and collection storage may change | `vitec_columns` resolution reads the table from TCA (`foreign_table`) — do **not** hard‑code CB tables | +| `georgringer/news ^14` | 10 News CTypes hard‑mapped in `NewsJsonRenderer` | Keep the CType→layout map in one place; re‑verify on major news upgrade | +| `b13/container ^3.1` | child linkage via `tx_container_parent`; page‑module grid required | Documented limitation: containers cannot be authored inside IRRE (Annex B‑2) | + +### 10.5 Modifiability — mandatory rules distilled +1. Container CTypes **shall** use `<` (copy), never `=<` (reference) — see Annex B‑1. +2. Table/field names **shall** be class constants, not inline literals. +3. Renderers **shall** reuse shared resolver services once they exist. +4. Magic numeric literals (e.g. a hard‑coded parent‑category uid) **shall** be replaced + by named constants or configuration. + +--- + +## 11 Conformance + +An implementation conforms to this specification if, for every content type it +exposes: +- **C1** the output validates as RFC 8259 JSON and matches the envelope of 7.2; +- **C2** the responsible renderer satisfies the class shape of 9.1 and the dual‑entry + pattern of 9.3; +- **C3** page‑id discovery follows 9.4 and error handling follows 9.5; +- **C4** payload keys follow 9.6 and the header section follows 9.8; +- **C5** container derivation follows the `<`‑copy rule of 6.4/10.5(1); +- **C6** any container‑nestable plugin is registered per 9.7. + +Deviations are recorded in Annex B and **shall** carry a remediation plan. + +--- + +## Annex A (normative) — Checklist: adding a new headless plugin + +1. **Model/TCA/SQL** — create the domain table and TCA; define table/field names as + constants. +2. **Serializer** — add `Classes/Service/Serializer` with `serializeListItem()` + and/or `serializeDetail()`; reuse existing resolver services. +3. **Renderer** — add `Classes/UserFunc/JsonRenderer` per 9.1, delegating + to the serializer; implement `render()` (9.3/9.4) and `renderForRecord()`. +4. **TypoScript** — in `Configuration/Sets/Vitecset/setup.typoscript`: + `tt_content. < lib.contentElementWithHeader` and + `content.fields. = USER` + `.userFunc = …->render`. +5. **Header section** — ensure the `headers` palette is present (9.8). +6. **Nesting** — if the plugin may sit inside a container, register it in + `PLUGIN_RENDERERS` in **both** the resolver and the processor (9.7). +7. **Deploy** — `vendor/bin/typo3 database:updateschema "*.add,*.change"` then + `vendor/bin/typo3 cache:flush`. +8. **Verify** — fetch the page JSON; confirm envelope (C1), keys (C4) and nested output. + +--- + +## Annex B (informative) — Nonconformity and technical‑debt register + +The following items were identified during architecture analysis. Items marked +*(to verify)* were reported by static review and **shall** be confirmed before +remediation. + +- **B‑1 — `=<` on Event/News CTypes.** `vitec_eventlist` and `news_pi1` use the + reference operator `=< lib.contentElementWithHeader`. Reference *to a `lib.*` object* + is used by headless itself and is generally safe; however, for consistency with + 10.5(1) these are VERIFIED SAFE and left as-is (references to lib.* are idiomatic in headless; only tt_content-to-tt_content references are unsafe, and those are already `<` for containers). +- **B‑2 — Containers cannot be authored inline (IRRE).** b13 container children live in + `tx_container_parent` and require the page‑module grid; they cannot be created inside + an inline field. This is a platform limitation, not a defect. The Success Story + "Columns" Content Block (`vitec_columns`) is the sanctioned inline alternative. +- **B‑3 — Duplicated `PLUGIN_RENDERERS` map** in `ContentElementResolver` and + `ContainerChildrenProcessor` (9.7/10.3). +- **B‑4 — Inline duplication** of image/category/MM/page‑id logic (10.2). +- **B‑5 — Hard‑coded literals** — model/MM table names, a `/fileadmin` prefix for + event JSON‑LD image URLs (`PageJsonLdRenderer`), and at least one magic + parent‑category uid appear as inline literals across download/datasheet/structured‑data + code *(to verify and extract to constants)*. `COLPOS_TO_COLUMN` in + `ContainerChildrenProcessor` must be kept in sync with the container TCA colPos values. +- **B‑6 — `DownloadcardcollectionJsonRenderer`**: two misplaced thumbnail output lines referenced an undefined variable in the image resolver. FIXED 2026-07-09 (removed; the PDF-thumbnail method is unaffected). +- **B‑7 — v13→v14 `CType`/`list_type` compatibility branches** in some download/ + datasheet renderers FIXED 2026-07-09: the unsatisfiable legacy OR branch was removed; the query now filters on the v14 CType only. + +--- + +## Annex C (informative) — Related documents + +- `Documentation/HeadlessIntegration.md` — informal how‑to (superseded as the + authoritative reference by this document). +- Header convention — the uniform header section across CEs, plugins and containers. +- `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point. + +*End of document EVO‑VITEC‑HL‑001 v1.0.* diff --git a/packages/vitec/Resources/Private/Language/locallang_buttons.xlf b/packages/vitec/Resources/Private/Language/locallang_buttons.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf b/packages/vitec/Resources/Private/Language/locallang_containers.xlf old mode 100755 new mode 100644 index aea755f..2ae841d --- a/packages/vitec/Resources/Private/Language/locallang_containers.xlf +++ b/packages/vitec/Resources/Private/Language/locallang_containers.xlf @@ -111,6 +111,73 @@ Sidebar (33%) + + VITEC · Cards Carousel + + + Horizontal carousel/slider. Place VITEC Card elements (or other content) inside; arrows, dots, autoplay and slides-per-view are configurable. + + + Slides + + + Carousel Settings + + + Align + + + flex-start + + + center + + + flex-end + + + stretch + + + Justify + + + flex-start + + + center + + + space-between + + + space-around + + + space-evenly + + + Grid + + + Gap + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + diff --git a/packages/vitec/Resources/Private/Language/locallang_pages.xlf b/packages/vitec/Resources/Private/Language/locallang_pages.xlf old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Private/Templates/Downloadcard/Backendpreview.html b/packages/vitec/Resources/Private/Templates/Downloadcard/Backendpreview.html old mode 100755 new mode 100644 index c904f43..785b0fc --- a/packages/vitec/Resources/Private/Templates/Downloadcard/Backendpreview.html +++ b/packages/vitec/Resources/Private/Templates/Downloadcard/Backendpreview.html @@ -13,16 +13,16 @@ {data.header} - Download Card (kein Header) + Download Card (no header)
- Konfiguration im FlexForm: - Produkt oder Download wählen, - Layout-Variante setzen. + Configured via FlexForm: + Select a product or download, + set the layout variant.
diff --git a/packages/vitec/Resources/Private/Templates/Downloadcardcollection/Backendpreview.html b/packages/vitec/Resources/Private/Templates/Downloadcardcollection/Backendpreview.html old mode 100755 new mode 100644 index 01f01ca..5fb3bf7 --- a/packages/vitec/Resources/Private/Templates/Downloadcardcollection/Backendpreview.html +++ b/packages/vitec/Resources/Private/Templates/Downloadcardcollection/Backendpreview.html @@ -13,15 +13,15 @@ {data.header} - Download Card Collection (kein Header) + Download Card Collection (no header)
- Multi-Download-Auswahl mit optionalem Header-Image. - Konfiguration im FlexForm. + Multi-download selection with optional header image. + Configured via FlexForm.
diff --git a/packages/vitec/Resources/Public/Css/backend-container-collapse.css b/packages/vitec/Resources/Public/Css/backend-container-collapse.css new file mode 100644 index 0000000..8d3e19b --- /dev/null +++ b/packages/vitec/Resources/Public/Css/backend-container-collapse.css @@ -0,0 +1,22 @@ +/* ============================================================= + VITEC — container collapse/expand in the Page module + ============================================================= */ + +/* Hide the container body (nested child columns) when collapsed. */ +.t3-page-ce.vitec-ce-collapsed > .t3-page-ce-element > .t3-page-ce-body { + display: none; +} + +/* Optional: dim/round the collapsed container header a touch. */ +.t3-page-ce.vitec-ce-collapsed > .t3-page-ce-element > .t3-page-ce-header { + border-bottom: 1px dashed var(--typo3-component-border-color, #ccc); +} + +/* Chevron rotates when collapsed. */ +.vitec-collapse-toggle typo3-backend-icon { + display: inline-flex; + transition: transform 0.15s ease-in-out; +} +.t3-page-ce.vitec-ce-collapsed .vitec-collapse-toggle typo3-backend-icon { + transform: rotate(180deg); +} diff --git a/packages/vitec/Resources/Public/Css/backend-preview.css b/packages/vitec/Resources/Public/Css/backend-preview.css old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/tx_vitec_domain_model_event.svg b/packages/vitec/Resources/Public/Icons/tx_vitec_domain_model_event.svg new file mode 100644 index 0000000..1762f93 --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/tx_vitec_domain_model_event.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/vitec/Resources/Public/Icons/vitec-cards-carousel.svg b/packages/vitec/Resources/Public/Icons/vitec-cards-carousel.svg new file mode 100644 index 0000000..ba359b5 --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-cards-carousel.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/vitec/Resources/Public/Icons/vitec-cols-25-25-25-25.svg b/packages/vitec/Resources/Public/Icons/vitec-cols-25-25-25-25.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-cols-33-33-33.svg b/packages/vitec/Resources/Public/Icons/vitec-cols-33-33-33.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-cols-33-66.svg b/packages/vitec/Resources/Public/Icons/vitec-cols-33-66.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-cols-50-50.svg b/packages/vitec/Resources/Public/Icons/vitec-cols-50-50.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-cols-66-33.svg b/packages/vitec/Resources/Public/Icons/vitec-cols-66-33.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-container.svg b/packages/vitec/Resources/Public/Icons/vitec-container.svg old mode 100755 new mode 100644 diff --git a/packages/vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg b/packages/vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg new file mode 100644 index 0000000..2ad65d5 --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/vitec/Resources/Public/Icons/vitec-plugin-eventlist.svg b/packages/vitec/Resources/Public/Icons/vitec-plugin-eventlist.svg new file mode 100644 index 0000000..59de362 --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-plugin-eventlist.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/vitec/Resources/Public/Javascript/container-collapse.js b/packages/vitec/Resources/Public/Javascript/container-collapse.js new file mode 100644 index 0000000..e61a74b --- /dev/null +++ b/packages/vitec/Resources/Public/Javascript/container-collapse.js @@ -0,0 +1,131 @@ +/** + * VITEC — collapse/expand toggle for container content elements in the + * TYPO3 v14 Page module. + * + * Adds a chevron button into the `.btn-toolbar` of every VITEC container + * (vitec_container, vitec_cols_*, vitec_cards_carousel — anything that + * renders a nested b13 grid). Clicking it hides/shows the container body + * (the nested child columns). State is persisted per element uid in + * localStorage, so it survives reloads. + * + * Pure DOM enhancement — no core or b13 template overrides. + */ +class VitecContainerCollapse { + constructor() { + this.storagePrefix = 'vitec-ce-collapsed-'; + this.ready(); + } + + ready() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.boot()); + } else { + this.boot(); + } + } + + boot() { + this.processAll(); + // Page module can re-render columns via AJAX; watch for new elements. + const root = document.querySelector('.t3js-page-columns-wrapper, .module, body'); + if (root) { + const observer = new MutationObserver(() => { + window.clearTimeout(this._debounce); + this._debounce = window.setTimeout(() => this.processAll(), 150); + }); + observer.observe(root, { childList: true, subtree: true }); + } + } + + processAll() { + document.querySelectorAll('.t3-page-ce[data-uid]').forEach((ce) => this.enhance(ce)); + } + + isCollapsed(uid) { + try { + return window.localStorage.getItem(this.storagePrefix + uid) === '1'; + } catch (e) { + return false; + } + } + + setCollapsed(uid, collapsed) { + try { + if (collapsed) { + window.localStorage.setItem(this.storagePrefix + uid, '1'); + } else { + window.localStorage.removeItem(this.storagePrefix + uid); + } + } catch (e) { + /* storage unavailable — ignore */ + } + } + + enhance(ce) { + if (ce.dataset.vitecCollapseReady === '1') { + return; + } + + // The direct body of THIS element (not a nested child's body). + const element = ce.querySelector(':scope > .t3-page-ce-element'); + if (!element) { + return; + } + const body = element.querySelector(':scope > .t3-page-ce-body'); + if (!body) { + return; + } + + // A container is recognised by a nested b13 grid inside its body. + const hasNestedGrid = body.querySelector('.t3-grid-container') !== null; + if (!hasNestedGrid) { + return; + } + + // Best-effort VITEC filter via the header icon identifier + // (b13 sets the icon identifier to the CType, e.g. "vitec_container"). + const iconEl = ce.querySelector( + '.t3-page-ce-header-left typo3-backend-icon[identifier], .t3-page-ce-header-left [data-identifier]' + ); + if (iconEl) { + const identifier = + iconEl.getAttribute('identifier') || iconEl.getAttribute('data-identifier') || ''; + if (identifier && identifier.indexOf('vitec') === -1) { + return; // a non-VITEC container — leave it alone + } + } + + const toolbar = element.querySelector( + ':scope > .t3-page-ce-header .btn-toolbar .btn-group' + ); + if (!toolbar) { + return; + } + + ce.dataset.vitecCollapseReady = '1'; + const uid = ce.dataset.uid; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'btn btn-sm btn-default btn-borderless vitec-collapse-toggle'; + button.title = 'Collapse / expand container'; + button.setAttribute('aria-label', 'Collapse or expand container'); + button.innerHTML = + ''; + + toolbar.insertBefore(button, toolbar.firstChild); + + if (this.isCollapsed(uid)) { + ce.classList.add('vitec-ce-collapsed'); + } + + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + const collapsed = ce.classList.toggle('vitec-ce-collapsed'); + this.setCollapsed(uid, collapsed); + }); + } +} + +export default new VitecContainerCollapse(); diff --git a/packages/vitec/Resources/Public/Javascript/structured-data-generator.js b/packages/vitec/Resources/Public/Javascript/structured-data-generator.js old mode 100755 new mode 100644 diff --git a/packages/vitec/ext_localconf.php b/packages/vitec/ext_localconf.php old mode 100755 new mode 100644 index b57ed44..c2e64b0 --- a/packages/vitec/ext_localconf.php +++ b/packages/vitec/ext_localconf.php @@ -147,4 +147,14 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1750000000] = [ ] ); + ExtensionUtility::configurePlugin( + 'Vitec', + 'Eventlist', + [ + \Evomedien\Vitec\Controller\EventController::class => 'list' + ], + [ + \Evomedien\Vitec\Controller\EventController::class => 'list' + ] + ); })(); diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql old mode 100755 new mode 100644 index dc321ca..97a8c56 --- a/packages/vitec/ext_tables.sql +++ b/packages/vitec/ext_tables.sql @@ -93,27 +93,51 @@ CREATE TABLE tx_vitec_product_related_mm ( ); CREATE TABLE tx_vitec_domain_model_usecase ( - uid int(11) NOT NULL auto_increment, - pid int(11) DEFAULT '0' NOT NULL, - tstamp int(11) DEFAULT '0' NOT NULL, - crdate int(11) DEFAULT '0' NOT NULL, - cruser_id int(11) DEFAULT '0' NOT NULL, - deleted tinyint(4) DEFAULT '0' NOT NULL, - hidden tinyint(4) DEFAULT '0' NOT NULL, - starttime int(11) DEFAULT '0' NOT NULL, - endtime int(11) DEFAULT '0' NOT NULL, title varchar(255) DEFAULT '' NOT NULL, slug varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - description text, - caseimage INTEGER, - logoimage INTEGER, - singlepid varchar(255) DEFAULT '' NOT NULL, + subtitle varchar(255) DEFAULT '' NOT NULL, + + card_image int(11) unsigned DEFAULT '0' NOT NULL, + customer_logo int(11) unsigned DEFAULT '0' NOT NULL, + teaser text, + market int(11) unsigned DEFAULT '0' NOT NULL, + + hero_bgimage int(11) unsigned DEFAULT '0' NOT NULL, + hero_small_image int(11) unsigned DEFAULT '0' NOT NULL, + hero_video int(11) unsigned DEFAULT '0' NOT NULL, + hero_overlay_color varchar(20) DEFAULT '' NOT NULL, + hero_overlay_opacity decimal(3,2) DEFAULT '0.00' NOT NULL, + + content_elements int(11) unsigned DEFAULT '0' NOT NULL, + + solutions int(11) unsigned DEFAULT '0' NOT NULL, + products int(11) unsigned DEFAULT '0' NOT NULL, + categories int(11) unsigned DEFAULT '0' NOT NULL, + + seo_title varchar(255) DEFAULT '' NOT NULL, + seo_description text, + no_index smallint(5) unsigned DEFAULT '0' NOT NULL, + no_follow smallint(5) unsigned DEFAULT '0' NOT NULL, + canonical_link varchar(1024) DEFAULT '' NOT NULL, + og_title varchar(255) DEFAULT '' NOT NULL, + og_description text, + og_image int(11) unsigned DEFAULT '0' NOT NULL, + twitter_title varchar(255) DEFAULT '' NOT NULL, + twitter_description text, + twitter_image int(11) unsigned DEFAULT '0' NOT NULL, + + layout_variant varchar(30) DEFAULT 'standard' NOT NULL, + background_variant varchar(20) DEFAULT 'none' NOT NULL, + accent_color varchar(20) DEFAULT '' NOT NULL, hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid) + hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, + featured smallint(5) unsigned DEFAULT '0' NOT NULL, + show_related smallint(5) unsigned DEFAULT '1' NOT NULL, + hero_layout varchar(20) DEFAULT 'fullscreen' NOT NULL, + text_theme varchar(20) DEFAULT 'light' NOT NULL, + + description text, + singlepid varchar(255) DEFAULT '' NOT NULL ); CREATE TABLE tx_vitec_domain_model_solution ( @@ -169,5 +193,53 @@ ADD type VARCHAR(255) DEFAULT '' NOT NULL; CREATE TABLE tt_content ( - tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL + tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL, + tx_vitec_gap VARCHAR(5) DEFAULT '3' NOT NULL, + tx_vitec_col1_align VARCHAR(20) DEFAULT 'stretch' NOT NULL, + tx_vitec_col1_justify VARCHAR(20) DEFAULT 'flex-start' NOT NULL, + tx_vitec_col2_align VARCHAR(20) DEFAULT 'stretch' NOT NULL, + tx_vitec_col2_justify VARCHAR(20) DEFAULT 'flex-start' NOT NULL, + tx_vitec_col3_align VARCHAR(20) DEFAULT 'stretch' NOT NULL, + tx_vitec_col3_justify VARCHAR(20) DEFAULT 'flex-start' NOT NULL, + tx_vitec_col4_align VARCHAR(20) DEFAULT 'stretch' NOT NULL, + tx_vitec_col4_justify VARCHAR(20) DEFAULT 'flex-start' NOT NULL, + tx_vitec_usecase_content int(11) unsigned DEFAULT '0' NOT NULL +); + +CREATE TABLE tx_vitec_domain_model_event ( + title varchar(255) DEFAULT '' NOT NULL, + slug varchar(255) DEFAULT '' NOT NULL, + teaser varchar(255) DEFAULT '' NOT NULL, + description text, + eventstart int(11) DEFAULT '0' NOT NULL, + eventend int(11) DEFAULT '0' NOT NULL, + venue varchar(255) DEFAULT '' NOT NULL, + booth varchar(255) DEFAULT '' NOT NULL, + city varchar(255) DEFAULT '' NOT NULL, + country varchar(255) DEFAULT '' NOT NULL, + attendancemode varchar(20) DEFAULT 'offline' NOT NULL, + eventstatus varchar(20) DEFAULT 'scheduled' NOT NULL, + eventurl varchar(1024) DEFAULT '' NOT NULL, + meetinglink varchar(1024) DEFAULT '' NOT NULL, + image int(11) unsigned DEFAULT '0' NOT NULL, + hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, + hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL +); + +CREATE TABLE tx_vitec_usecase_solution_mm ( + uid_local int(11) unsigned DEFAULT '0' NOT NULL, + uid_foreign int(11) unsigned DEFAULT '0' NOT NULL, + sorting int(11) unsigned DEFAULT '0' NOT NULL, + sorting_foreign int(11) unsigned DEFAULT '0' NOT NULL, + KEY uid_local (uid_local), + KEY uid_foreign (uid_foreign) +); + +CREATE TABLE tx_vitec_usecase_product_mm ( + uid_local int(11) unsigned DEFAULT '0' NOT NULL, + uid_foreign int(11) unsigned DEFAULT '0' NOT NULL, + sorting int(11) unsigned DEFAULT '0' NOT NULL, + sorting_foreign int(11) unsigned DEFAULT '0' NOT NULL, + KEY uid_local (uid_local), + KEY uid_foreign (uid_foreign) ); diff --git a/public/.htaccess b/public/.htaccess index 522d097..6e6d24e 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -318,6 +318,11 @@ AddDefaultCharset utf-8 RewriteCond %{SCRIPT_FILENAME} -f RewriteRule (?:^|/)\. - [F] + # Redirect missing hardcoded Collateral file URLs to a resolver that + # chooses the newest available version by filename convention. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^fileadmin/downloads/Collateral/(.+)$ %{ENV:CWD}collateral-fallback.php?requested=$1 [QSA,L,NC] + # Stop rewrite processing, if we are in any known directory # NOTE: Add your additional local storages here RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|uploads/) - [L] @@ -392,7 +397,7 @@ FileETag None # Add your own rules here. -AuthUserFile "/usr/www/users/vitecevo/live/public/.htpasswd" -AuthName "DEV" -AuthType Basic -require valid-user +#AuthUserFile "/usr/www/users/vitecevo/live/public/.htpasswd" +#AuthName "DEV" +#AuthType Basic +#require valid-user diff --git a/public/_frontend/.htaccess b/public/_frontend/.htaccess index 3ca08b9..15135c6 100644 --- a/public/_frontend/.htaccess +++ b/public/_frontend/.htaccess @@ -9,8 +9,19 @@ RewriteRule ^ - [L] RewriteRule ^ index.html [L] +# ── Cache headers ──────────────────────────────────────────────────────────── + +# index.html: never cache — always fetch fresh so the browser picks up +# the latest hashed JS/CSS filenames after a deploy. - Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Cache-Control "no-store, no-cache, must-revalidate" Header set Pragma "no-cache" - Header set Expires 0 - \ No newline at end of file + Header set Expires "0" + + +# Hashed assets (JS, CSS, fonts, images inside /assets/): +# Vite appends a content hash to every filename, so these are immutable. +# Cache them for 1 year — a new deploy produces new filenames automatically. + + Header set Cache-Control "public, max-age=31536000, immutable" + diff --git a/public/_frontend/assets/VitecDebugToggle-9pp4AW9x.js b/public/_frontend/assets/VitecDebugToggle-9pp4AW9x.js deleted file mode 100644 index 4346a66..0000000 --- a/public/_frontend/assets/VitecDebugToggle-9pp4AW9x.js +++ /dev/null @@ -1 +0,0 @@ -import{B as e,E as t,F as n,I as r,K as i,L as a,M as o,N as s,O as c,P as l,R as u,U as d,V as f,j as p,k as m,r as h,w as g,z as _}from"./index-BI31wk8i.js";var v=i(d());function y(e){let{controlled:t,default:n,name:r,state:i=`value`}=e,{current:a}=v.useRef(t!==void 0),[o,s]=v.useState(n);return[a?t:o,v.useCallback(e=>{a||s(e)},[])]}var b=y,x=v.createContext(void 0);function S(){return v.useContext(x)}function C(e){return _(`PrivateSwitchBase`,e)}u(`PrivateSwitchBase`,[`root`,`checked`,`disabled`,`input`,`edgeStart`,`edgeEnd`]);var w=f(),T=e=>{let{classes:t,checked:n,disabled:r,edge:i}=e;return a({root:[`root`,n&&`checked`,r&&`disabled`,i&&`edge${l(i)}`],input:[`input`]},C,t)},E=n(m,{name:`MuiSwitchBase`})({padding:9,borderRadius:`50%`,variants:[{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:({edge:e,ownerState:t})=>e===`start`&&t.size!==`small`,style:{marginLeft:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}},{props:({edge:e,ownerState:t})=>e===`end`&&t.size!==`small`,style:{marginRight:-12}}]}),D=n(`input`,{name:`MuiSwitchBase`,shouldForwardProp:r})({cursor:`inherit`,position:`absolute`,opacity:0,width:`100%`,height:`100%`,top:0,left:0,margin:0,padding:0,zIndex:1}),O=v.forwardRef(function(e,t){let{autoFocus:n,checked:r,checkedIcon:i,defaultChecked:a,disabled:o,disableFocusRipple:s=!1,edge:c=!1,icon:l,id:u,inputProps:d,inputRef:f,name:m,onBlur:h,onChange:g,onFocus:_,readOnly:v,required:y=!1,tabIndex:x,type:C,value:O,slots:k={},slotProps:A={},...j}=e,[M,N]=b({controlled:r,default:!!a,name:`SwitchBase`,state:`checked`}),P=S(),F=e=>{_&&_(e),P&&P.onFocus&&P.onFocus(e)},I=e=>{h&&h(e),P&&P.onBlur&&P.onBlur(e)},L=e=>{if(e.nativeEvent.defaultPrevented)return;let t=e.target.checked;N(t),g&&g(e,t)},R=o;P&&R===void 0&&(R=P.disabled);let z=C===`checkbox`||C===`radio`,B={...e,checked:M,disabled:R,disableFocusRipple:s,edge:c},V=T(B),H={slots:k,slotProps:{input:d,...A}},[U,W]=p(`root`,{ref:t,elementType:E,className:V.root,shouldForwardComponentProp:!0,externalForwardedProps:{...H,component:`span`,...j},getSlotProps:e=>({...e,onFocus:t=>{e.onFocus?.(t),F(t)},onBlur:t=>{e.onBlur?.(t),I(t)}}),ownerState:B,additionalProps:{centerRipple:!0,focusRipple:!s,disabled:R,role:void 0,tabIndex:null}}),[G,K]=p(`input`,{ref:f,elementType:D,className:V.input,externalForwardedProps:H,getSlotProps:e=>({...e,onChange:t=>{e.onChange?.(t),L(t)}}),ownerState:B,additionalProps:{autoFocus:n,checked:r,defaultChecked:a,disabled:R,id:z?u:void 0,name:m,readOnly:v,required:y,tabIndex:x,type:C,...C===`checkbox`&&O===void 0?{}:{value:O}}});return(0,w.jsxs)(U,{...W,children:[(0,w.jsx)(G,{...K}),M?i:l]})});function k(e){return _(`MuiSwitch`,e)}var A=u(`MuiSwitch`,[`root`,`edgeStart`,`edgeEnd`,`switchBase`,`colorPrimary`,`colorSecondary`,`sizeSmall`,`sizeMedium`,`checked`,`disabled`,`input`,`thumb`,`track`]),j=e=>{let{classes:t,edge:n,size:r,color:i,checked:o,disabled:s}=e,c=a({root:[`root`,n&&`edge${l(n)}`,`size${l(r)}`],switchBase:[`switchBase`,`color${l(i)}`,o&&`checked`,s&&`disabled`],thumb:[`thumb`],track:[`track`],input:[`input`]},k,t);return{...t,...c}},M=n(`span`,{name:`MuiSwitch`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.edge&&t[`edge${l(n.edge)}`],t[`size${l(n.size)}`]]}})({display:`inline-flex`,width:58,height:38,overflow:`hidden`,padding:12,boxSizing:`border-box`,position:`relative`,flexShrink:0,zIndex:0,verticalAlign:`middle`,"@media print":{colorAdjust:`exact`},variants:[{props:{edge:`start`},style:{marginLeft:-8}},{props:{edge:`end`},style:{marginRight:-8}},{props:{size:`small`},style:{width:40,height:24,padding:7,[`& .${A.thumb}`]:{width:16,height:16},[`& .${A.switchBase}`]:{padding:4,[`&.${A.checked}`]:{transform:`translateX(16px)`}}}}]}),N=n(O,{name:`MuiSwitch`,slot:`SwitchBase`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.switchBase,{[`& .${A.input}`]:t.input},n.color!==`default`&&t[`color${l(n.color)}`]]}})(s(({theme:e})=>({position:`absolute`,top:0,left:0,zIndex:1,color:e.vars?e.vars.palette.Switch.defaultColor:`${e.palette.mode===`light`?e.palette.common.white:e.palette.grey[300]}`,transition:e.transitions.create([`left`,`transform`],{duration:e.transitions.duration.shortest}),[`&.${A.checked}`]:{transform:`translateX(20px)`},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch.defaultDisabledColor:`${e.palette.mode===`light`?e.palette.grey[100]:e.palette.grey[600]}`},[`&.${A.checked} + .${A.track}`]:{opacity:.5},[`&.${A.disabled} + .${A.track}`]:{opacity:e.vars?e.vars.opacity.switchTrackDisabled:`${e.palette.mode===`light`?.12:.2}`},[`& .${A.input}`]:{left:`-100%`,width:`300%`}})),s(({theme:e})=>({"&:hover":{backgroundColor:e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},variants:[...Object.entries(e.palette).filter(c([`light`])).map(([t])=>({props:{color:t},style:{[`&.${A.checked}`]:{color:(e.vars||e).palette[t].main,"&:hover":{backgroundColor:e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch[`${t}DisabledColor`]:`${e.palette.mode===`light`?e.lighten(e.palette[t].main,.62):e.darken(e.palette[t].main,.55)}`}},[`&.${A.checked} + .${A.track}`]:{backgroundColor:(e.vars||e).palette[t].main}}}))]}))),P=n(`span`,{name:`MuiSwitch`,slot:`Track`})(s(({theme:e})=>({height:`100%`,width:`100%`,borderRadius:14/2,zIndex:-1,transition:e.transitions.create([`opacity`,`background-color`],{duration:e.transitions.duration.shortest}),backgroundColor:e.vars?e.vars.palette.common.onBackground:`${e.palette.mode===`light`?e.palette.common.black:e.palette.common.white}`,opacity:e.vars?e.vars.opacity.switchTrack:`${e.palette.mode===`light`?.38:.3}`}))),F=n(`span`,{name:`MuiSwitch`,slot:`Thumb`})(s(({theme:e})=>({boxShadow:(e.vars||e).shadows[1],backgroundColor:`currentColor`,width:20,height:20,borderRadius:`50%`}))),I=v.forwardRef(function(t,n){let r=o({props:t,name:`MuiSwitch`}),{className:i,color:a=`primary`,edge:s=!1,size:c=`medium`,sx:l,slots:u={},slotProps:d={},...f}=r,m={...r,color:a,edge:s,size:c},h=j(m),g={slots:u,slotProps:d},[_,v]=p(`root`,{className:e(h.root,i),elementType:M,externalForwardedProps:g,ownerState:m,additionalProps:{sx:l}}),[y,b]=p(`thumb`,{className:h.thumb,elementType:F,externalForwardedProps:g,ownerState:m}),x=(0,w.jsx)(y,{...b}),[S,C]=p(`track`,{className:h.track,elementType:P,externalForwardedProps:g,ownerState:m});return(0,w.jsxs)(_,{...v,children:[(0,w.jsx)(N,{type:`checkbox`,icon:x,checkedIcon:x,ref:n,ownerState:m,...f,classes:{...h,root:h.switchBase},slots:{...u.switchBase&&{root:u.switchBase},...u.input&&{input:u.input}},slotProps:{...d.switchBase&&{root:typeof d.switchBase==`function`?d.switchBase(m):d.switchBase},input:{role:`switch`},...d.input&&{input:typeof d.input==`function`?d.input(m):d.input}}}),(0,w.jsx)(S,{...C})]})});function L({enabled:e,onChange:n}){return h?(0,w.jsxs)(g,{sx:{position:`fixed`,bottom:`1rem`,right:`1rem`,zIndex:9999,display:`flex`,alignItems:`center`,gap:`0.5rem`,bgcolor:`background.paper`,border:`1px solid`,borderColor:`divider`,borderRadius:`2rem`,px:`0.75rem`,py:`0.25rem`,boxShadow:3},children:[(0,w.jsx)(t,{variant:`caption`,sx:{userSelect:`none`,fontWeight:600,letterSpacing:`0.05em`},children:`Debug`}),(0,w.jsx)(I,{size:`small`,checked:e,onChange:(e,t)=>n(t)})]}):null}export{L as VitecDebugToggle}; \ No newline at end of file diff --git a/public/_frontend/assets/VitecDebugToggle-BzU_nymF.js b/public/_frontend/assets/VitecDebugToggle-BzU_nymF.js deleted file mode 100644 index 768adce..0000000 --- a/public/_frontend/assets/VitecDebugToggle-BzU_nymF.js +++ /dev/null @@ -1 +0,0 @@ -import{B as e,E as t,F as n,I as r,K as i,L as a,M as o,N as s,O as c,P as l,R as u,U as d,V as f,j as p,k as m,r as h,w as g,z as _}from"./index-jouuqNaB.js";var v=i(d());function y(e){let{controlled:t,default:n,name:r,state:i=`value`}=e,{current:a}=v.useRef(t!==void 0),[o,s]=v.useState(n);return[a?t:o,v.useCallback(e=>{a||s(e)},[])]}var b=y,x=v.createContext(void 0);function S(){return v.useContext(x)}function C(e){return _(`PrivateSwitchBase`,e)}u(`PrivateSwitchBase`,[`root`,`checked`,`disabled`,`input`,`edgeStart`,`edgeEnd`]);var w=f(),T=e=>{let{classes:t,checked:n,disabled:r,edge:i}=e;return a({root:[`root`,n&&`checked`,r&&`disabled`,i&&`edge${l(i)}`],input:[`input`]},C,t)},E=n(m,{name:`MuiSwitchBase`})({padding:9,borderRadius:`50%`,variants:[{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:({edge:e,ownerState:t})=>e===`start`&&t.size!==`small`,style:{marginLeft:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}},{props:({edge:e,ownerState:t})=>e===`end`&&t.size!==`small`,style:{marginRight:-12}}]}),D=n(`input`,{name:`MuiSwitchBase`,shouldForwardProp:r})({cursor:`inherit`,position:`absolute`,opacity:0,width:`100%`,height:`100%`,top:0,left:0,margin:0,padding:0,zIndex:1}),O=v.forwardRef(function(e,t){let{autoFocus:n,checked:r,checkedIcon:i,defaultChecked:a,disabled:o,disableFocusRipple:s=!1,edge:c=!1,icon:l,id:u,inputProps:d,inputRef:f,name:m,onBlur:h,onChange:g,onFocus:_,readOnly:v,required:y=!1,tabIndex:x,type:C,value:O,slots:k={},slotProps:A={},...j}=e,[M,N]=b({controlled:r,default:!!a,name:`SwitchBase`,state:`checked`}),P=S(),F=e=>{_&&_(e),P&&P.onFocus&&P.onFocus(e)},I=e=>{h&&h(e),P&&P.onBlur&&P.onBlur(e)},L=e=>{if(e.nativeEvent.defaultPrevented)return;let t=e.target.checked;N(t),g&&g(e,t)},R=o;P&&R===void 0&&(R=P.disabled);let z=C===`checkbox`||C===`radio`,B={...e,checked:M,disabled:R,disableFocusRipple:s,edge:c},V=T(B),H={slots:k,slotProps:{input:d,...A}},[U,W]=p(`root`,{ref:t,elementType:E,className:V.root,shouldForwardComponentProp:!0,externalForwardedProps:{...H,component:`span`,...j},getSlotProps:e=>({...e,onFocus:t=>{e.onFocus?.(t),F(t)},onBlur:t=>{e.onBlur?.(t),I(t)}}),ownerState:B,additionalProps:{centerRipple:!0,focusRipple:!s,disabled:R,role:void 0,tabIndex:null}}),[G,K]=p(`input`,{ref:f,elementType:D,className:V.input,externalForwardedProps:H,getSlotProps:e=>({...e,onChange:t=>{e.onChange?.(t),L(t)}}),ownerState:B,additionalProps:{autoFocus:n,checked:r,defaultChecked:a,disabled:R,id:z?u:void 0,name:m,readOnly:v,required:y,tabIndex:x,type:C,...C===`checkbox`&&O===void 0?{}:{value:O}}});return(0,w.jsxs)(U,{...W,children:[(0,w.jsx)(G,{...K}),M?i:l]})});function k(e){return _(`MuiSwitch`,e)}var A=u(`MuiSwitch`,[`root`,`edgeStart`,`edgeEnd`,`switchBase`,`colorPrimary`,`colorSecondary`,`sizeSmall`,`sizeMedium`,`checked`,`disabled`,`input`,`thumb`,`track`]),j=e=>{let{classes:t,edge:n,size:r,color:i,checked:o,disabled:s}=e,c=a({root:[`root`,n&&`edge${l(n)}`,`size${l(r)}`],switchBase:[`switchBase`,`color${l(i)}`,o&&`checked`,s&&`disabled`],thumb:[`thumb`],track:[`track`],input:[`input`]},k,t);return{...t,...c}},M=n(`span`,{name:`MuiSwitch`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.edge&&t[`edge${l(n.edge)}`],t[`size${l(n.size)}`]]}})({display:`inline-flex`,width:58,height:38,overflow:`hidden`,padding:12,boxSizing:`border-box`,position:`relative`,flexShrink:0,zIndex:0,verticalAlign:`middle`,"@media print":{colorAdjust:`exact`},variants:[{props:{edge:`start`},style:{marginLeft:-8}},{props:{edge:`end`},style:{marginRight:-8}},{props:{size:`small`},style:{width:40,height:24,padding:7,[`& .${A.thumb}`]:{width:16,height:16},[`& .${A.switchBase}`]:{padding:4,[`&.${A.checked}`]:{transform:`translateX(16px)`}}}}]}),N=n(O,{name:`MuiSwitch`,slot:`SwitchBase`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.switchBase,{[`& .${A.input}`]:t.input},n.color!==`default`&&t[`color${l(n.color)}`]]}})(s(({theme:e})=>({position:`absolute`,top:0,left:0,zIndex:1,color:e.vars?e.vars.palette.Switch.defaultColor:`${e.palette.mode===`light`?e.palette.common.white:e.palette.grey[300]}`,transition:e.transitions.create([`left`,`transform`],{duration:e.transitions.duration.shortest}),[`&.${A.checked}`]:{transform:`translateX(20px)`},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch.defaultDisabledColor:`${e.palette.mode===`light`?e.palette.grey[100]:e.palette.grey[600]}`},[`&.${A.checked} + .${A.track}`]:{opacity:.5},[`&.${A.disabled} + .${A.track}`]:{opacity:e.vars?e.vars.opacity.switchTrackDisabled:`${e.palette.mode===`light`?.12:.2}`},[`& .${A.input}`]:{left:`-100%`,width:`300%`}})),s(({theme:e})=>({"&:hover":{backgroundColor:e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},variants:[...Object.entries(e.palette).filter(c([`light`])).map(([t])=>({props:{color:t},style:{[`&.${A.checked}`]:{color:(e.vars||e).palette[t].main,"&:hover":{backgroundColor:e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch[`${t}DisabledColor`]:`${e.palette.mode===`light`?e.lighten(e.palette[t].main,.62):e.darken(e.palette[t].main,.55)}`}},[`&.${A.checked} + .${A.track}`]:{backgroundColor:(e.vars||e).palette[t].main}}}))]}))),P=n(`span`,{name:`MuiSwitch`,slot:`Track`})(s(({theme:e})=>({height:`100%`,width:`100%`,borderRadius:14/2,zIndex:-1,transition:e.transitions.create([`opacity`,`background-color`],{duration:e.transitions.duration.shortest}),backgroundColor:e.vars?e.vars.palette.common.onBackground:`${e.palette.mode===`light`?e.palette.common.black:e.palette.common.white}`,opacity:e.vars?e.vars.opacity.switchTrack:`${e.palette.mode===`light`?.38:.3}`}))),F=n(`span`,{name:`MuiSwitch`,slot:`Thumb`})(s(({theme:e})=>({boxShadow:(e.vars||e).shadows[1],backgroundColor:`currentColor`,width:20,height:20,borderRadius:`50%`}))),I=v.forwardRef(function(t,n){let r=o({props:t,name:`MuiSwitch`}),{className:i,color:a=`primary`,edge:s=!1,size:c=`medium`,sx:l,slots:u={},slotProps:d={},...f}=r,m={...r,color:a,edge:s,size:c},h=j(m),g={slots:u,slotProps:d},[_,v]=p(`root`,{className:e(h.root,i),elementType:M,externalForwardedProps:g,ownerState:m,additionalProps:{sx:l}}),[y,b]=p(`thumb`,{className:h.thumb,elementType:F,externalForwardedProps:g,ownerState:m}),x=(0,w.jsx)(y,{...b}),[S,C]=p(`track`,{className:h.track,elementType:P,externalForwardedProps:g,ownerState:m});return(0,w.jsxs)(_,{...v,children:[(0,w.jsx)(N,{type:`checkbox`,icon:x,checkedIcon:x,ref:n,ownerState:m,...f,classes:{...h,root:h.switchBase},slots:{...u.switchBase&&{root:u.switchBase},...u.input&&{input:u.input}},slotProps:{...d.switchBase&&{root:typeof d.switchBase==`function`?d.switchBase(m):d.switchBase},input:{role:`switch`},...d.input&&{input:typeof d.input==`function`?d.input(m):d.input}}}),(0,w.jsx)(S,{...C})]})});function L({enabled:e,onChange:n}){return h?(0,w.jsxs)(g,{sx:{position:`fixed`,bottom:`1rem`,right:`1rem`,zIndex:9999,display:`flex`,alignItems:`center`,gap:`0.5rem`,bgcolor:`background.paper`,border:`1px solid`,borderColor:`divider`,borderRadius:`2rem`,px:`0.75rem`,py:`0.25rem`,boxShadow:3},children:[(0,w.jsx)(t,{variant:`caption`,sx:{userSelect:`none`,fontWeight:600,letterSpacing:`0.05em`},children:`Debug`}),(0,w.jsx)(I,{size:`small`,checked:e,onChange:(e,t)=>n(t)})]}):null}export{L as VitecDebugToggle}; \ No newline at end of file diff --git a/public/_frontend/assets/VitecDebugToggle-C76AUijU.js b/public/_frontend/assets/VitecDebugToggle-C76AUijU.js new file mode 100644 index 0000000..e923825 --- /dev/null +++ b/public/_frontend/assets/VitecDebugToggle-C76AUijU.js @@ -0,0 +1 @@ +import{t as e}from"./useControlled-OB0I4oAK.js";import{B as t,D as n,H as r,J as i,M as a,P as o,Q as s,U as c,W as l,X as u,Y as d,Z as f,j as p,k as m,q as h,r as g,tt as _,z as v}from"./index-7LYai_r-.js";var y=_(s()),b=y.createContext(void 0);function x(){return y.useContext(b)}function S(e){return d(`PrivateSwitchBase`,e)}i(`PrivateSwitchBase`,[`root`,`checked`,`disabled`,`input`,`edgeStart`,`edgeEnd`]);var C=f(),w=e=>{let{classes:n,checked:r,disabled:i,edge:a}=e;return h({root:[`root`,r&&`checked`,i&&`disabled`,a&&`edge${t(a)}`],input:[`input`]},S,n)},T=c(a,{name:`MuiSwitchBase`})({padding:9,borderRadius:`50%`,variants:[{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:({edge:e,ownerState:t})=>e===`start`&&t.size!==`small`,style:{marginLeft:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}},{props:({edge:e,ownerState:t})=>e===`end`&&t.size!==`small`,style:{marginRight:-12}}]}),E=c(`input`,{name:`MuiSwitchBase`,shouldForwardProp:l})({cursor:`inherit`,position:`absolute`,opacity:0,width:`100%`,height:`100%`,top:0,left:0,margin:0,padding:0,zIndex:1}),D=y.forwardRef(function(t,n){let{autoFocus:r,checked:i,checkedIcon:a,defaultChecked:s,disabled:c,disableFocusRipple:l=!1,edge:u=!1,icon:d,id:f,inputProps:p,inputRef:m,name:h,onBlur:g,onChange:_,onFocus:v,readOnly:y,required:b=!1,tabIndex:S,type:D,value:O,slots:k={},slotProps:A={},...j}=t,[M,N]=e({controlled:i,default:!!s,name:`SwitchBase`,state:`checked`}),P=x(),F=e=>{v&&v(e),P&&P.onFocus&&P.onFocus(e)},I=e=>{g&&g(e),P&&P.onBlur&&P.onBlur(e)},L=e=>{if(e.nativeEvent.defaultPrevented)return;let t=e.target.checked;N(t),_&&_(e,t)},R=c;P&&R===void 0&&(R=P.disabled);let z=D===`checkbox`||D===`radio`,B={...t,checked:M,disabled:R,disableFocusRipple:l,edge:u},V=w(B),H={slots:k,slotProps:{input:p,...A}},[U,W]=o(`root`,{ref:n,elementType:T,className:V.root,shouldForwardComponentProp:!0,externalForwardedProps:{...H,component:`span`,...j},getSlotProps:e=>({...e,onFocus:t=>{e.onFocus?.(t),F(t)},onBlur:t=>{e.onBlur?.(t),I(t)}}),ownerState:B,additionalProps:{centerRipple:!0,focusRipple:!l,disabled:R,role:void 0,tabIndex:null}}),[G,K]=o(`input`,{ref:m,elementType:E,className:V.input,externalForwardedProps:H,getSlotProps:e=>({...e,onChange:t=>{e.onChange?.(t),L(t)}}),ownerState:B,additionalProps:{autoFocus:r,checked:i,defaultChecked:s,disabled:R,id:z?f:void 0,name:h,readOnly:y,required:b,tabIndex:S,type:D,...D===`checkbox`&&O===void 0?{}:{value:O}}});return(0,C.jsxs)(U,{...W,children:[(0,C.jsx)(G,{...K}),M?a:d]})});function O(e){return d(`MuiSwitch`,e)}var k=i(`MuiSwitch`,[`root`,`edgeStart`,`edgeEnd`,`switchBase`,`colorPrimary`,`colorSecondary`,`sizeSmall`,`sizeMedium`,`checked`,`disabled`,`input`,`thumb`,`track`]),A=e=>{let{classes:n,edge:r,size:i,color:a,checked:o,disabled:s}=e,c=h({root:[`root`,r&&`edge${t(r)}`,`size${t(i)}`],switchBase:[`switchBase`,`color${t(a)}`,o&&`checked`,s&&`disabled`],thumb:[`thumb`],track:[`track`],input:[`input`]},O,n);return{...n,...c}},j=c(`span`,{name:`MuiSwitch`,slot:`Root`,overridesResolver:(e,n)=>{let{ownerState:r}=e;return[n.root,r.edge&&n[`edge${t(r.edge)}`],n[`size${t(r.size)}`]]}})({display:`inline-flex`,width:58,height:38,overflow:`hidden`,padding:12,boxSizing:`border-box`,position:`relative`,flexShrink:0,zIndex:0,verticalAlign:`middle`,"@media print":{colorAdjust:`exact`},variants:[{props:{edge:`start`},style:{marginLeft:-8}},{props:{edge:`end`},style:{marginRight:-8}},{props:{size:`small`},style:{width:40,height:24,padding:7,[`& .${k.thumb}`]:{width:16,height:16},[`& .${k.switchBase}`]:{padding:4,[`&.${k.checked}`]:{transform:`translateX(16px)`}}}}]}),M=c(D,{name:`MuiSwitch`,slot:`SwitchBase`,overridesResolver:(e,n)=>{let{ownerState:r}=e;return[n.switchBase,{[`& .${k.input}`]:n.input},r.color!==`default`&&n[`color${t(r.color)}`]]}})(v(({theme:e})=>({position:`absolute`,top:0,left:0,zIndex:1,color:e.vars?e.vars.palette.Switch.defaultColor:`${e.palette.mode===`light`?e.palette.common.white:e.palette.grey[300]}`,transition:e.transitions.create([`left`,`transform`],{duration:e.transitions.duration.shortest}),[`&.${k.checked}`]:{transform:`translateX(20px)`},[`&.${k.disabled}`]:{color:e.vars?e.vars.palette.Switch.defaultDisabledColor:`${e.palette.mode===`light`?e.palette.grey[100]:e.palette.grey[600]}`},[`&.${k.checked} + .${k.track}`]:{opacity:.5},[`&.${k.disabled} + .${k.track}`]:{opacity:e.vars?e.vars.opacity.switchTrackDisabled:`${e.palette.mode===`light`?.12:.2}`},[`& .${k.input}`]:{left:`-100%`,width:`300%`}})),v(({theme:e})=>({"&:hover":{backgroundColor:e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},variants:[...Object.entries(e.palette).filter(p([`light`])).map(([t])=>({props:{color:t},style:{[`&.${k.checked}`]:{color:(e.vars||e).palette[t].main,"&:hover":{backgroundColor:e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},[`&.${k.disabled}`]:{color:e.vars?e.vars.palette.Switch[`${t}DisabledColor`]:`${e.palette.mode===`light`?e.lighten(e.palette[t].main,.62):e.darken(e.palette[t].main,.55)}`}},[`&.${k.checked} + .${k.track}`]:{backgroundColor:(e.vars||e).palette[t].main}}}))]}))),N=c(`span`,{name:`MuiSwitch`,slot:`Track`})(v(({theme:e})=>({height:`100%`,width:`100%`,borderRadius:14/2,zIndex:-1,transition:e.transitions.create([`opacity`,`background-color`],{duration:e.transitions.duration.shortest}),backgroundColor:e.vars?e.vars.palette.common.onBackground:`${e.palette.mode===`light`?e.palette.common.black:e.palette.common.white}`,opacity:e.vars?e.vars.opacity.switchTrack:`${e.palette.mode===`light`?.38:.3}`}))),P=c(`span`,{name:`MuiSwitch`,slot:`Thumb`})(v(({theme:e})=>({boxShadow:(e.vars||e).shadows[1],backgroundColor:`currentColor`,width:20,height:20,borderRadius:`50%`}))),F=y.forwardRef(function(e,t){let n=r({props:e,name:`MuiSwitch`}),{className:i,color:a=`primary`,edge:s=!1,size:c=`medium`,sx:l,slots:d={},slotProps:f={},...p}=n,m={...n,color:a,edge:s,size:c},h=A(m),g={slots:d,slotProps:f},[_,v]=o(`root`,{className:u(h.root,i),elementType:j,externalForwardedProps:g,ownerState:m,additionalProps:{sx:l}}),[y,b]=o(`thumb`,{className:h.thumb,elementType:P,externalForwardedProps:g,ownerState:m}),x=(0,C.jsx)(y,{...b}),[S,w]=o(`track`,{className:h.track,elementType:N,externalForwardedProps:g,ownerState:m});return(0,C.jsxs)(_,{...v,children:[(0,C.jsx)(M,{type:`checkbox`,icon:x,checkedIcon:x,ref:t,ownerState:m,...p,classes:{...h,root:h.switchBase},slots:{...d.switchBase&&{root:d.switchBase},...d.input&&{input:d.input}},slotProps:{...f.switchBase&&{root:typeof f.switchBase==`function`?f.switchBase(m):f.switchBase},input:{role:`switch`},...f.input&&{input:typeof f.input==`function`?f.input(m):f.input}}}),(0,C.jsx)(S,{...w})]})});function I({enabled:e,onChange:t}){return g?(0,C.jsxs)(n,{sx:{position:`fixed`,bottom:`1rem`,right:`1rem`,zIndex:9999,display:`flex`,alignItems:`center`,gap:`0.5rem`,bgcolor:`background.paper`,border:`1px solid`,borderColor:`divider`,borderRadius:`2rem`,px:`0.75rem`,py:`0.25rem`,boxShadow:3},children:[(0,C.jsx)(m,{variant:`caption`,sx:{userSelect:`none`,fontWeight:600,letterSpacing:`0.05em`},children:`Debug`}),(0,C.jsx)(F,{size:`small`,checked:e,onChange:(e,n)=>t(n)})]}):null}export{I as VitecDebugToggle}; \ No newline at end of file diff --git a/public/_frontend/assets/VitecDebugToggle-C8gYxgVq.js b/public/_frontend/assets/VitecDebugToggle-C8gYxgVq.js deleted file mode 100644 index 3fa9794..0000000 --- a/public/_frontend/assets/VitecDebugToggle-C8gYxgVq.js +++ /dev/null @@ -1 +0,0 @@ -import{B as e,E as t,F as n,I as r,K as i,L as a,M as o,N as s,O as c,P as l,R as u,U as d,V as f,j as p,k as m,r as h,w as g,z as _}from"./index-BXbyImyv.js";var v=i(d());function y(e){let{controlled:t,default:n,name:r,state:i=`value`}=e,{current:a}=v.useRef(t!==void 0),[o,s]=v.useState(n);return[a?t:o,v.useCallback(e=>{a||s(e)},[])]}var b=y,x=v.createContext(void 0);function S(){return v.useContext(x)}function C(e){return _(`PrivateSwitchBase`,e)}u(`PrivateSwitchBase`,[`root`,`checked`,`disabled`,`input`,`edgeStart`,`edgeEnd`]);var w=f(),T=e=>{let{classes:t,checked:n,disabled:r,edge:i}=e;return a({root:[`root`,n&&`checked`,r&&`disabled`,i&&`edge${l(i)}`],input:[`input`]},C,t)},E=n(m,{name:`MuiSwitchBase`})({padding:9,borderRadius:`50%`,variants:[{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:({edge:e,ownerState:t})=>e===`start`&&t.size!==`small`,style:{marginLeft:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}},{props:({edge:e,ownerState:t})=>e===`end`&&t.size!==`small`,style:{marginRight:-12}}]}),D=n(`input`,{name:`MuiSwitchBase`,shouldForwardProp:r})({cursor:`inherit`,position:`absolute`,opacity:0,width:`100%`,height:`100%`,top:0,left:0,margin:0,padding:0,zIndex:1}),O=v.forwardRef(function(e,t){let{autoFocus:n,checked:r,checkedIcon:i,defaultChecked:a,disabled:o,disableFocusRipple:s=!1,edge:c=!1,icon:l,id:u,inputProps:d,inputRef:f,name:m,onBlur:h,onChange:g,onFocus:_,readOnly:v,required:y=!1,tabIndex:x,type:C,value:O,slots:k={},slotProps:A={},...j}=e,[M,N]=b({controlled:r,default:!!a,name:`SwitchBase`,state:`checked`}),P=S(),F=e=>{_&&_(e),P&&P.onFocus&&P.onFocus(e)},I=e=>{h&&h(e),P&&P.onBlur&&P.onBlur(e)},L=e=>{if(e.nativeEvent.defaultPrevented)return;let t=e.target.checked;N(t),g&&g(e,t)},R=o;P&&R===void 0&&(R=P.disabled);let z=C===`checkbox`||C===`radio`,B={...e,checked:M,disabled:R,disableFocusRipple:s,edge:c},V=T(B),H={slots:k,slotProps:{input:d,...A}},[U,W]=p(`root`,{ref:t,elementType:E,className:V.root,shouldForwardComponentProp:!0,externalForwardedProps:{...H,component:`span`,...j},getSlotProps:e=>({...e,onFocus:t=>{e.onFocus?.(t),F(t)},onBlur:t=>{e.onBlur?.(t),I(t)}}),ownerState:B,additionalProps:{centerRipple:!0,focusRipple:!s,disabled:R,role:void 0,tabIndex:null}}),[G,K]=p(`input`,{ref:f,elementType:D,className:V.input,externalForwardedProps:H,getSlotProps:e=>({...e,onChange:t=>{e.onChange?.(t),L(t)}}),ownerState:B,additionalProps:{autoFocus:n,checked:r,defaultChecked:a,disabled:R,id:z?u:void 0,name:m,readOnly:v,required:y,tabIndex:x,type:C,...C===`checkbox`&&O===void 0?{}:{value:O}}});return(0,w.jsxs)(U,{...W,children:[(0,w.jsx)(G,{...K}),M?i:l]})});function k(e){return _(`MuiSwitch`,e)}var A=u(`MuiSwitch`,[`root`,`edgeStart`,`edgeEnd`,`switchBase`,`colorPrimary`,`colorSecondary`,`sizeSmall`,`sizeMedium`,`checked`,`disabled`,`input`,`thumb`,`track`]),j=e=>{let{classes:t,edge:n,size:r,color:i,checked:o,disabled:s}=e,c=a({root:[`root`,n&&`edge${l(n)}`,`size${l(r)}`],switchBase:[`switchBase`,`color${l(i)}`,o&&`checked`,s&&`disabled`],thumb:[`thumb`],track:[`track`],input:[`input`]},k,t);return{...t,...c}},M=n(`span`,{name:`MuiSwitch`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.edge&&t[`edge${l(n.edge)}`],t[`size${l(n.size)}`]]}})({display:`inline-flex`,width:58,height:38,overflow:`hidden`,padding:12,boxSizing:`border-box`,position:`relative`,flexShrink:0,zIndex:0,verticalAlign:`middle`,"@media print":{colorAdjust:`exact`},variants:[{props:{edge:`start`},style:{marginLeft:-8}},{props:{edge:`end`},style:{marginRight:-8}},{props:{size:`small`},style:{width:40,height:24,padding:7,[`& .${A.thumb}`]:{width:16,height:16},[`& .${A.switchBase}`]:{padding:4,[`&.${A.checked}`]:{transform:`translateX(16px)`}}}}]}),N=n(O,{name:`MuiSwitch`,slot:`SwitchBase`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.switchBase,{[`& .${A.input}`]:t.input},n.color!==`default`&&t[`color${l(n.color)}`]]}})(s(({theme:e})=>({position:`absolute`,top:0,left:0,zIndex:1,color:e.vars?e.vars.palette.Switch.defaultColor:`${e.palette.mode===`light`?e.palette.common.white:e.palette.grey[300]}`,transition:e.transitions.create([`left`,`transform`],{duration:e.transitions.duration.shortest}),[`&.${A.checked}`]:{transform:`translateX(20px)`},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch.defaultDisabledColor:`${e.palette.mode===`light`?e.palette.grey[100]:e.palette.grey[600]}`},[`&.${A.checked} + .${A.track}`]:{opacity:.5},[`&.${A.disabled} + .${A.track}`]:{opacity:e.vars?e.vars.opacity.switchTrackDisabled:`${e.palette.mode===`light`?.12:.2}`},[`& .${A.input}`]:{left:`-100%`,width:`300%`}})),s(({theme:e})=>({"&:hover":{backgroundColor:e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},variants:[...Object.entries(e.palette).filter(c([`light`])).map(([t])=>({props:{color:t},style:{[`&.${A.checked}`]:{color:(e.vars||e).palette[t].main,"&:hover":{backgroundColor:e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch[`${t}DisabledColor`]:`${e.palette.mode===`light`?e.lighten(e.palette[t].main,.62):e.darken(e.palette[t].main,.55)}`}},[`&.${A.checked} + .${A.track}`]:{backgroundColor:(e.vars||e).palette[t].main}}}))]}))),P=n(`span`,{name:`MuiSwitch`,slot:`Track`})(s(({theme:e})=>({height:`100%`,width:`100%`,borderRadius:14/2,zIndex:-1,transition:e.transitions.create([`opacity`,`background-color`],{duration:e.transitions.duration.shortest}),backgroundColor:e.vars?e.vars.palette.common.onBackground:`${e.palette.mode===`light`?e.palette.common.black:e.palette.common.white}`,opacity:e.vars?e.vars.opacity.switchTrack:`${e.palette.mode===`light`?.38:.3}`}))),F=n(`span`,{name:`MuiSwitch`,slot:`Thumb`})(s(({theme:e})=>({boxShadow:(e.vars||e).shadows[1],backgroundColor:`currentColor`,width:20,height:20,borderRadius:`50%`}))),I=v.forwardRef(function(t,n){let r=o({props:t,name:`MuiSwitch`}),{className:i,color:a=`primary`,edge:s=!1,size:c=`medium`,sx:l,slots:u={},slotProps:d={},...f}=r,m={...r,color:a,edge:s,size:c},h=j(m),g={slots:u,slotProps:d},[_,v]=p(`root`,{className:e(h.root,i),elementType:M,externalForwardedProps:g,ownerState:m,additionalProps:{sx:l}}),[y,b]=p(`thumb`,{className:h.thumb,elementType:F,externalForwardedProps:g,ownerState:m}),x=(0,w.jsx)(y,{...b}),[S,C]=p(`track`,{className:h.track,elementType:P,externalForwardedProps:g,ownerState:m});return(0,w.jsxs)(_,{...v,children:[(0,w.jsx)(N,{type:`checkbox`,icon:x,checkedIcon:x,ref:n,ownerState:m,...f,classes:{...h,root:h.switchBase},slots:{...u.switchBase&&{root:u.switchBase},...u.input&&{input:u.input}},slotProps:{...d.switchBase&&{root:typeof d.switchBase==`function`?d.switchBase(m):d.switchBase},input:{role:`switch`},...d.input&&{input:typeof d.input==`function`?d.input(m):d.input}}}),(0,w.jsx)(S,{...C})]})});function L({enabled:e,onChange:n}){return h?(0,w.jsxs)(g,{sx:{position:`fixed`,bottom:`1rem`,right:`1rem`,zIndex:9999,display:`flex`,alignItems:`center`,gap:`0.5rem`,bgcolor:`background.paper`,border:`1px solid`,borderColor:`divider`,borderRadius:`2rem`,px:`0.75rem`,py:`0.25rem`,boxShadow:3},children:[(0,w.jsx)(t,{variant:`caption`,sx:{userSelect:`none`,fontWeight:600,letterSpacing:`0.05em`},children:`Debug`}),(0,w.jsx)(I,{size:`small`,checked:e,onChange:(e,t)=>n(t)})]}):null}export{L as VitecDebugToggle}; \ No newline at end of file diff --git a/public/_frontend/assets/VitecDebugToggle-D50ebvSn.js b/public/_frontend/assets/VitecDebugToggle-D50ebvSn.js deleted file mode 100644 index 91ced12..0000000 --- a/public/_frontend/assets/VitecDebugToggle-D50ebvSn.js +++ /dev/null @@ -1 +0,0 @@ -import{B as e,E as t,F as n,I as r,K as i,L as a,M as o,N as s,O as c,P as l,R as u,U as d,V as f,j as p,k as m,r as h,w as g,z as _}from"./index-Cq4vmvLV.js";var v=i(d());function y(e){let{controlled:t,default:n,name:r,state:i=`value`}=e,{current:a}=v.useRef(t!==void 0),[o,s]=v.useState(n);return[a?t:o,v.useCallback(e=>{a||s(e)},[])]}var b=y,x=v.createContext(void 0);function S(){return v.useContext(x)}function C(e){return _(`PrivateSwitchBase`,e)}u(`PrivateSwitchBase`,[`root`,`checked`,`disabled`,`input`,`edgeStart`,`edgeEnd`]);var w=f(),T=e=>{let{classes:t,checked:n,disabled:r,edge:i}=e;return a({root:[`root`,n&&`checked`,r&&`disabled`,i&&`edge${l(i)}`],input:[`input`]},C,t)},E=n(m,{name:`MuiSwitchBase`})({padding:9,borderRadius:`50%`,variants:[{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:({edge:e,ownerState:t})=>e===`start`&&t.size!==`small`,style:{marginLeft:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}},{props:({edge:e,ownerState:t})=>e===`end`&&t.size!==`small`,style:{marginRight:-12}}]}),D=n(`input`,{name:`MuiSwitchBase`,shouldForwardProp:r})({cursor:`inherit`,position:`absolute`,opacity:0,width:`100%`,height:`100%`,top:0,left:0,margin:0,padding:0,zIndex:1}),O=v.forwardRef(function(e,t){let{autoFocus:n,checked:r,checkedIcon:i,defaultChecked:a,disabled:o,disableFocusRipple:s=!1,edge:c=!1,icon:l,id:u,inputProps:d,inputRef:f,name:m,onBlur:h,onChange:g,onFocus:_,readOnly:v,required:y=!1,tabIndex:x,type:C,value:O,slots:k={},slotProps:A={},...j}=e,[M,N]=b({controlled:r,default:!!a,name:`SwitchBase`,state:`checked`}),P=S(),F=e=>{_&&_(e),P&&P.onFocus&&P.onFocus(e)},I=e=>{h&&h(e),P&&P.onBlur&&P.onBlur(e)},L=e=>{if(e.nativeEvent.defaultPrevented)return;let t=e.target.checked;N(t),g&&g(e,t)},R=o;P&&R===void 0&&(R=P.disabled);let z=C===`checkbox`||C===`radio`,B={...e,checked:M,disabled:R,disableFocusRipple:s,edge:c},V=T(B),H={slots:k,slotProps:{input:d,...A}},[U,W]=p(`root`,{ref:t,elementType:E,className:V.root,shouldForwardComponentProp:!0,externalForwardedProps:{...H,component:`span`,...j},getSlotProps:e=>({...e,onFocus:t=>{e.onFocus?.(t),F(t)},onBlur:t=>{e.onBlur?.(t),I(t)}}),ownerState:B,additionalProps:{centerRipple:!0,focusRipple:!s,disabled:R,role:void 0,tabIndex:null}}),[G,K]=p(`input`,{ref:f,elementType:D,className:V.input,externalForwardedProps:H,getSlotProps:e=>({...e,onChange:t=>{e.onChange?.(t),L(t)}}),ownerState:B,additionalProps:{autoFocus:n,checked:r,defaultChecked:a,disabled:R,id:z?u:void 0,name:m,readOnly:v,required:y,tabIndex:x,type:C,...C===`checkbox`&&O===void 0?{}:{value:O}}});return(0,w.jsxs)(U,{...W,children:[(0,w.jsx)(G,{...K}),M?i:l]})});function k(e){return _(`MuiSwitch`,e)}var A=u(`MuiSwitch`,[`root`,`edgeStart`,`edgeEnd`,`switchBase`,`colorPrimary`,`colorSecondary`,`sizeSmall`,`sizeMedium`,`checked`,`disabled`,`input`,`thumb`,`track`]),j=e=>{let{classes:t,edge:n,size:r,color:i,checked:o,disabled:s}=e,c=a({root:[`root`,n&&`edge${l(n)}`,`size${l(r)}`],switchBase:[`switchBase`,`color${l(i)}`,o&&`checked`,s&&`disabled`],thumb:[`thumb`],track:[`track`],input:[`input`]},k,t);return{...t,...c}},M=n(`span`,{name:`MuiSwitch`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.edge&&t[`edge${l(n.edge)}`],t[`size${l(n.size)}`]]}})({display:`inline-flex`,width:58,height:38,overflow:`hidden`,padding:12,boxSizing:`border-box`,position:`relative`,flexShrink:0,zIndex:0,verticalAlign:`middle`,"@media print":{colorAdjust:`exact`},variants:[{props:{edge:`start`},style:{marginLeft:-8}},{props:{edge:`end`},style:{marginRight:-8}},{props:{size:`small`},style:{width:40,height:24,padding:7,[`& .${A.thumb}`]:{width:16,height:16},[`& .${A.switchBase}`]:{padding:4,[`&.${A.checked}`]:{transform:`translateX(16px)`}}}}]}),N=n(O,{name:`MuiSwitch`,slot:`SwitchBase`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.switchBase,{[`& .${A.input}`]:t.input},n.color!==`default`&&t[`color${l(n.color)}`]]}})(s(({theme:e})=>({position:`absolute`,top:0,left:0,zIndex:1,color:e.vars?e.vars.palette.Switch.defaultColor:`${e.palette.mode===`light`?e.palette.common.white:e.palette.grey[300]}`,transition:e.transitions.create([`left`,`transform`],{duration:e.transitions.duration.shortest}),[`&.${A.checked}`]:{transform:`translateX(20px)`},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch.defaultDisabledColor:`${e.palette.mode===`light`?e.palette.grey[100]:e.palette.grey[600]}`},[`&.${A.checked} + .${A.track}`]:{opacity:.5},[`&.${A.disabled} + .${A.track}`]:{opacity:e.vars?e.vars.opacity.switchTrackDisabled:`${e.palette.mode===`light`?.12:.2}`},[`& .${A.input}`]:{left:`-100%`,width:`300%`}})),s(({theme:e})=>({"&:hover":{backgroundColor:e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},variants:[...Object.entries(e.palette).filter(c([`light`])).map(([t])=>({props:{color:t},style:{[`&.${A.checked}`]:{color:(e.vars||e).palette[t].main,"&:hover":{backgroundColor:e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:`transparent`}},[`&.${A.disabled}`]:{color:e.vars?e.vars.palette.Switch[`${t}DisabledColor`]:`${e.palette.mode===`light`?e.lighten(e.palette[t].main,.62):e.darken(e.palette[t].main,.55)}`}},[`&.${A.checked} + .${A.track}`]:{backgroundColor:(e.vars||e).palette[t].main}}}))]}))),P=n(`span`,{name:`MuiSwitch`,slot:`Track`})(s(({theme:e})=>({height:`100%`,width:`100%`,borderRadius:14/2,zIndex:-1,transition:e.transitions.create([`opacity`,`background-color`],{duration:e.transitions.duration.shortest}),backgroundColor:e.vars?e.vars.palette.common.onBackground:`${e.palette.mode===`light`?e.palette.common.black:e.palette.common.white}`,opacity:e.vars?e.vars.opacity.switchTrack:`${e.palette.mode===`light`?.38:.3}`}))),F=n(`span`,{name:`MuiSwitch`,slot:`Thumb`})(s(({theme:e})=>({boxShadow:(e.vars||e).shadows[1],backgroundColor:`currentColor`,width:20,height:20,borderRadius:`50%`}))),I=v.forwardRef(function(t,n){let r=o({props:t,name:`MuiSwitch`}),{className:i,color:a=`primary`,edge:s=!1,size:c=`medium`,sx:l,slots:u={},slotProps:d={},...f}=r,m={...r,color:a,edge:s,size:c},h=j(m),g={slots:u,slotProps:d},[_,v]=p(`root`,{className:e(h.root,i),elementType:M,externalForwardedProps:g,ownerState:m,additionalProps:{sx:l}}),[y,b]=p(`thumb`,{className:h.thumb,elementType:F,externalForwardedProps:g,ownerState:m}),x=(0,w.jsx)(y,{...b}),[S,C]=p(`track`,{className:h.track,elementType:P,externalForwardedProps:g,ownerState:m});return(0,w.jsxs)(_,{...v,children:[(0,w.jsx)(N,{type:`checkbox`,icon:x,checkedIcon:x,ref:n,ownerState:m,...f,classes:{...h,root:h.switchBase},slots:{...u.switchBase&&{root:u.switchBase},...u.input&&{input:u.input}},slotProps:{...d.switchBase&&{root:typeof d.switchBase==`function`?d.switchBase(m):d.switchBase},input:{role:`switch`},...d.input&&{input:typeof d.input==`function`?d.input(m):d.input}}}),(0,w.jsx)(S,{...C})]})});function L({enabled:e,onChange:n}){return h?(0,w.jsxs)(g,{sx:{position:`fixed`,bottom:`1rem`,right:`1rem`,zIndex:9999,display:`flex`,alignItems:`center`,gap:`0.5rem`,bgcolor:`background.paper`,border:`1px solid`,borderColor:`divider`,borderRadius:`2rem`,px:`0.75rem`,py:`0.25rem`,boxShadow:3},children:[(0,w.jsx)(t,{variant:`caption`,sx:{userSelect:`none`,fontWeight:600,letterSpacing:`0.05em`},children:`Debug`}),(0,w.jsx)(I,{size:`small`,checked:e,onChange:(e,t)=>n(t)})]}):null}export{L as VitecDebugToggle}; \ No newline at end of file diff --git a/public/_frontend/assets/fa-brands-400-BK5CuonO.woff2 b/public/_frontend/assets/fa-brands-400-BK5CuonO.woff2 new file mode 100644 index 0000000..4aa6dea Binary files /dev/null and b/public/_frontend/assets/fa-brands-400-BK5CuonO.woff2 differ diff --git a/public/_frontend/assets/fa-regular-400-WLfxSHV3.woff2 b/public/_frontend/assets/fa-regular-400-WLfxSHV3.woff2 new file mode 100644 index 0000000..f6590ce Binary files /dev/null and b/public/_frontend/assets/fa-regular-400-WLfxSHV3.woff2 differ diff --git a/public/_frontend/assets/fa-solid-900-Iz_TytpP.woff2 b/public/_frontend/assets/fa-solid-900-Iz_TytpP.woff2 new file mode 100644 index 0000000..d7e38a2 Binary files /dev/null and b/public/_frontend/assets/fa-solid-900-Iz_TytpP.woff2 differ diff --git a/public/_frontend/assets/fa-v4compatibility-CMhGCN2W.woff2 b/public/_frontend/assets/fa-v4compatibility-CMhGCN2W.woff2 new file mode 100644 index 0000000..5491ff8 Binary files /dev/null and b/public/_frontend/assets/fa-v4compatibility-CMhGCN2W.woff2 differ diff --git a/public/_frontend/assets/header-BCPgsUEc.js b/public/_frontend/assets/header-BCPgsUEc.js deleted file mode 100644 index f510e88..0000000 --- a/public/_frontend/assets/header-BCPgsUEc.js +++ /dev/null @@ -1 +0,0 @@ -import{E as e,V as t,_ as n,b as r,c as i,d as a,h as o,l as s,m as c,n as l,p as u,u as d,x as f}from"./index-BXbyImyv.js";var p=t(),m={1:`h1`,2:`h2`,3:`h3`,4:`h4`,5:`h5`,6:`h6`};function h({element:t,className:h,nested:g,devInfoTags:_,uid:v,spaceBefore:y,spaceAfter:b}){let x=i(t),S=a(t),C=Number(s(t)??0),w=m[C]??`h3`,T=n(d(t)),E=T===`center`?`center`:T===`right`?`right`:`left`,D=T===`center`?`center`:T===`right`?`flex-end`:`flex-start`,O=u(t.content?.headerLink),k=o(t.content?.headerLink),A=c(t.content?.headerLink,`Mehr erfahren`);return C===100?null:(0,p.jsx)(l,{className:h,devInfoTags:_,nested:g,uid:v,spaceBefore:y,spaceAfter:b,children:(0,p.jsx)(`div`,{className:`header__inner`,children:(0,p.jsxs)(f,{spacing:.75,sx:{textAlign:E,alignItems:D},children:[x&&(0,p.jsx)(e,{variant:w,children:x}),S&&(0,p.jsx)(e,{variant:`subtitle1`,color:`text.secondary`,children:S}),O&&(0,p.jsx)(r,{href:O,target:k??`_self`,variant:`outlined`,size:`sm`,children:A})]})})})}export{h as VitecHeader}; \ No newline at end of file diff --git a/public/_frontend/assets/header-BwD3yTr8.js b/public/_frontend/assets/header-BwD3yTr8.js new file mode 100644 index 0000000..b0c79db --- /dev/null +++ b/public/_frontend/assets/header-BwD3yTr8.js @@ -0,0 +1 @@ +import{C as e,S as t,Z as n,_ as r,c as i,d as a,h as o,k as s,l as c,m as l,n as u,p as d,u as f,v as p,y as m}from"./index-7LYai_r-.js";var h=n();function g({element:n,className:g,nested:_,devInfoTags:v,uid:y,spaceBefore:b,spaceAfter:x}){let S=i(n),C=a(n),w=Number(c(n)??0),T=p(w),E=m(w),D=r(f(n)),O=D===`center`?`center`:D===`right`?`right`:`left`,k=D===`center`?`center`:D===`right`?`flex-end`:`flex-start`,A=d(n.content?.headerLink),j=o(n.content?.headerLink),M=l(n.content?.headerLink,`Mehr erfahren`);return w===100?null:(0,h.jsx)(u,{className:g,devInfoTags:v,nested:_,uid:y,spaceBefore:b,spaceAfter:x,children:(0,h.jsx)(`div`,{className:`header__inner`,children:(0,h.jsxs)(e,{spacing:.75,sx:{textAlign:O,alignItems:k},children:[S&&(0,h.jsx)(s,{variant:T,children:S}),C&&(0,h.jsx)(s,{variant:E,color:`text.secondary`,children:C}),A&&(0,h.jsx)(t,{href:A,target:j??`_self`,variant:`outlined`,size:`sm`,children:M})]})})})}export{g as VitecHeader}; \ No newline at end of file diff --git a/public/_frontend/assets/header-CMkDHNN8.js b/public/_frontend/assets/header-CMkDHNN8.js deleted file mode 100644 index fd7f998..0000000 --- a/public/_frontend/assets/header-CMkDHNN8.js +++ /dev/null @@ -1 +0,0 @@ -import{E as e,V as t,_ as n,b as r,c as i,d as a,h as o,l as s,m as c,n as l,p as u,u as d,x as f}from"./index-Cq4vmvLV.js";var p=t(),m={1:`h1`,2:`h2`,3:`h3`,4:`h4`,5:`h5`,6:`h6`};function h({element:t,className:h,nested:g,devInfoTags:_,uid:v,spaceBefore:y,spaceAfter:b}){let x=i(t),S=a(t),C=Number(s(t)??0),w=m[C]??`h3`,T=n(d(t)),E=T===`center`?`center`:T===`right`?`right`:`left`,D=T===`center`?`center`:T===`right`?`flex-end`:`flex-start`,O=u(t.content?.headerLink),k=o(t.content?.headerLink),A=c(t.content?.headerLink,`Mehr erfahren`);return C===100?null:(0,p.jsx)(l,{className:h,devInfoTags:_,nested:g,uid:v,spaceBefore:y,spaceAfter:b,children:(0,p.jsx)(`div`,{className:`header__inner`,children:(0,p.jsxs)(f,{spacing:.75,sx:{textAlign:E,alignItems:D},children:[x&&(0,p.jsx)(e,{variant:w,children:x}),S&&(0,p.jsx)(e,{variant:`subtitle1`,color:`text.secondary`,children:S}),O&&(0,p.jsx)(r,{href:O,target:k??`_self`,variant:`outlined`,size:`sm`,children:A})]})})})}export{h as VitecHeader}; \ No newline at end of file diff --git a/public/_frontend/assets/header-CbcGCvAq.js b/public/_frontend/assets/header-CbcGCvAq.js deleted file mode 100644 index e1d0a81..0000000 --- a/public/_frontend/assets/header-CbcGCvAq.js +++ /dev/null @@ -1 +0,0 @@ -import{E as e,V as t,_ as n,b as r,c as i,d as a,h as o,l as s,m as c,n as l,p as u,u as d,x as f}from"./index-jouuqNaB.js";var p=t(),m={1:`h1`,2:`h2`,3:`h3`,4:`h4`,5:`h5`,6:`h6`};function h({element:t,className:h,nested:g,devInfoTags:_,uid:v,spaceBefore:y,spaceAfter:b}){let x=i(t),S=a(t),C=Number(s(t)??0),w=m[C]??`h3`,T=n(d(t)),E=T===`center`?`center`:T===`right`?`right`:`left`,D=T===`center`?`center`:T===`right`?`flex-end`:`flex-start`,O=u(t.content?.headerLink),k=o(t.content?.headerLink),A=c(t.content?.headerLink,`Mehr erfahren`);return C===100?null:(0,p.jsx)(l,{className:h,devInfoTags:_,nested:g,uid:v,spaceBefore:y,spaceAfter:b,children:(0,p.jsx)(`div`,{className:`header__inner`,children:(0,p.jsxs)(f,{spacing:.75,sx:{textAlign:E,alignItems:D},children:[x&&(0,p.jsx)(e,{variant:w,children:x}),S&&(0,p.jsx)(e,{variant:`subtitle1`,color:`text.secondary`,children:S}),O&&(0,p.jsx)(r,{href:O,target:k??`_self`,variant:`outlined`,size:`sm`,children:A})]})})})}export{h as VitecHeader}; \ No newline at end of file diff --git a/public/_frontend/assets/header-DV30IKp8.js b/public/_frontend/assets/header-DV30IKp8.js deleted file mode 100644 index f7a1ff1..0000000 --- a/public/_frontend/assets/header-DV30IKp8.js +++ /dev/null @@ -1 +0,0 @@ -import{E as e,V as t,_ as n,b as r,c as i,d as a,h as o,l as s,m as c,n as l,p as u,u as d,x as f}from"./index-BI31wk8i.js";var p=t(),m={1:`h1`,2:`h2`,3:`h3`,4:`h4`,5:`h5`,6:`h6`};function h({element:t,className:h,nested:g,devInfoTags:_,uid:v,spaceBefore:y,spaceAfter:b}){let x=i(t),S=a(t),C=Number(s(t)??0),w=m[C]??`h3`,T=n(d(t)),E=T===`center`?`center`:T===`right`?`right`:`left`,D=T===`center`?`center`:T===`right`?`flex-end`:`flex-start`,O=u(t.content?.headerLink),k=o(t.content?.headerLink),A=c(t.content?.headerLink,`Mehr erfahren`);return C===100?null:(0,p.jsx)(l,{className:h,devInfoTags:_,nested:g,uid:v,spaceBefore:y,spaceAfter:b,children:(0,p.jsx)(`div`,{className:`header__inner`,children:(0,p.jsxs)(f,{spacing:.75,sx:{textAlign:E,alignItems:D},children:[x&&(0,p.jsx)(e,{variant:w,children:x}),S&&(0,p.jsx)(e,{variant:`subtitle1`,color:`text.secondary`,children:S}),O&&(0,p.jsx)(r,{href:O,target:k??`_self`,variant:`outlined`,size:`sm`,children:A})]})})})}export{h as VitecHeader}; \ No newline at end of file diff --git a/public/_frontend/assets/index-7LYai_r-.js b/public/_frontend/assets/index-7LYai_r-.js new file mode 100644 index 0000000..8286939 --- /dev/null +++ b/public/_frontend/assets/index-7LYai_r-.js @@ -0,0 +1,148 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vitec_downloadcardcollection-7D90DISQ.js","assets/jszip.min-B_jfdBDa.js","assets/vitec_datasheets-Bu643D_W.js","assets/vitec_faq-Bii0IQAm.js","assets/useControlled-OB0I4oAK.js","assets/VitecDebugToggle-C76AUijU.js"])))=>i.map(i=>d[i]); +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),l=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function.")});(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ee(e,t){return E(e.type,t,e.props)}function D(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function O(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var te=/\/+/g;function k(e,t){return typeof e==`object`&&e&&e.key!=null?O(``+e.key):t.toString(36)}function A(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function j(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,j(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+k(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(te,`$&/`)+`/`),j(o,r,i,``,function(e){return e})):o!=null&&(D(o)&&(o=ee(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(te,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,D());else{var t=n(l);t!==null&&k(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&k(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?D():S=!1}}}var D;if(typeof y==`function`)D=function(){y(ee)};else if(typeof MessageChannel<`u`){var O=new MessageChannel,te=O.port2;O.port1.onmessage=ee,D=function(){te.postMessage(null)}}else D=function(){_(ee,0)};function k(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,k(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,D()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1ae||(e.current=ie[ae],ie[ae]=null,ae--)}function F(e,t){ae++,ie[ae]=e.current,e.current=t}var ce=oe(null),le=oe(null),ue=oe(null),de=oe(null);function fe(e,t){switch(F(ue,t),F(le,e),F(ce,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Ud(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Ud(t),e=Wd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}se(ce),F(ce,e)}function pe(){se(ce),se(le),se(ue)}function me(e){e.memoizedState!==null&&F(de,e);var t=ce.current,n=Wd(t,e.type);t!==n&&(F(le,e),F(ce,n))}function he(e){le.current===e&&(se(ce),se(le)),de.current===e&&(se(de),$f._currentValue=re)}var ge,_e;function ve(e){if(ge===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ge=t&&t[1]||``,_e=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ye=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ve(n):``}function xe(e,t){switch(e.tag){case 26:case 27:case 5:return ve(e.type);case 16:return ve(`Lazy`);case 13:return e.child!==t&&t!==null?ve(`Suspense Fallback`):ve(`Suspense`);case 19:return ve(`SuspenseList`);case 0:case 15:return be(e.type,!1);case 11:return be(e.type.render,!1);case 1:return be(e.type,!0);case 31:return ve(`Activity`);default:return``}}function Se(e){try{var t=``,n=null;do t+=xe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var Ce=Object.prototype.hasOwnProperty,we=t.unstable_scheduleCallback,Te=t.unstable_cancelCallback,Ee=t.unstable_shouldYield,De=t.unstable_requestPaint,Oe=t.unstable_now,ke=t.unstable_getCurrentPriorityLevel,Ae=t.unstable_ImmediatePriority,je=t.unstable_UserBlockingPriority,Me=t.unstable_NormalPriority,Ne=t.unstable_LowPriority,Pe=t.unstable_IdlePriority,Fe=t.log,Ie=t.unstable_setDisableYieldValue,Le=null,Re=null;function ze(e){if(typeof Fe==`function`&&Ie(e),Re&&typeof Re.setStrictMode==`function`)try{Re.setStrictMode(Le,e)}catch{}}var Be=Math.clz32?Math.clz32:Ue,Ve=Math.log,He=Math.LN2;function Ue(e){return e>>>=0,e===0?32:31-(Ve(e)/He|0)|0}var We=256,Ge=262144,Ke=4194304;function qe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Je(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=qe(n))):i=qe(o):i=qe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=qe(n))):i=qe(o)):i=qe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ye(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Xe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ze(){var e=Ke;return Ke<<=1,!(Ke&62914560)&&(Ke=4194304),e}function Qe(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function $e(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function et(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),pn=!1;if(fn)try{var mn={};Object.defineProperty(mn,`passive`,{get:function(){pn=!0}}),window.addEventListener(`test`,mn,mn),window.removeEventListener(`test`,mn,mn)}catch{pn=!1}var hn=null,gn=null,_n=null;function vn(){if(_n)return _n;var e,t=gn,n=t.length,r,i=`value`in hn?hn.value:hn.textContent,a=i.length;for(e=0;e=Xn),$n=` `,er=!1;function tr(e,t){switch(e){case`keyup`:return Jn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function nr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var rr=!1;function ir(e,t){switch(e){case`compositionend`:return nr(t);case`keypress`:return t.which===32?(er=!0,$n):null;case`textInput`:return e=t.data,e===$n&&er?null:e;default:return null}}function ar(e,t){if(rr)return e===`compositionend`||!Yn&&tr(e,t)?(e=vn(),_n=gn=hn=null,rr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Dr(n)}}function kr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?kr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ar(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=zt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=zt(e.document)}return t}function jr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Mr=fn&&`documentMode`in document&&11>=document.documentMode,Nr=null,Pr=null,Fr=null,Ir=!1;function Lr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ir||Nr==null||Nr!==zt(r)||(r=Nr,`selectionStart`in r&&jr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fr&&Er(Fr,r)||(Fr=r,r=Dd(Pr,`onSelect`),0>=o,i-=o,Di=1<<32-Be(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),R&&ki(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),R&&ki(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return R&&ki(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),R&&ki(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===D&&ka(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ia(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=mi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=pi(o.type,o.key,o.props,null,e.mode,c),Ia(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=_i(o,e.mode,c),c.return=e,e=c}return s(e);case D:return o=ka(o),b(e,r,o,c)}if(M(o))return h(e,r,o,c);if(A(o)){if(l=A(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Fa(o),c);if(o.$$typeof===C)return b(e,r,ra(e,o),c);La(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=hi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Pa=0;var i=b(e,t,n,r);return Na=null,i}catch(t){if(t===Ca||t===Ta)throw t;var a=L(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var za=Ra(!0),Ba=Ra(!1),Va=!1;function Ha(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ua(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Wa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ga(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,W&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=si(e),oi(e,null,n),t}return ri(e,r,t,n),si(e)}function Ka(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,nt(e,n)}}function qa(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ja=!1;function Ya(){if(Ja){var e=ma;if(e!==null)throw e}}function Xa(e,t,n,r){Ja=!1;var i=e.updateQueue;Va=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(q&f)===f:(r&f)===f){f!==0&&f===pa&&(Ja=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Va=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Jl|=o,e.lanes=o,e.memoizedState=d}}function Za(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Qa(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=N.T,s={};N.T=s,Ls(e,!1,t,n);try{var c=i(),l=N.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Is(e,t,_a(c,r),gu(e)):Is(e,t,r,gu(e))}catch(n){Is(e,t,{then:function(){},status:`rejected`,reason:n},gu())}finally{P.p=a,o!==null&&s.types!==null&&(o.types=s.types),N.T=o}}function Es(){}function Ds(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Os(e).queue;Ts(e,a,t,re,n===null?Es:function(){return ks(e),n(r)})}function Os(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:re,baseState:re,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:re},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ks(e){var t=Os(e);t.next===null&&(t=e.alternate.memoizedState),Is(e,t.next.queue,{},gu())}function As(){return na($f)}function js(){return No().memoizedState}function Ms(){return No().memoizedState}function Ns(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=gu();e=Wa(n);var r=Ga(t,e,n);r!==null&&(vu(r,t,n),Ka(r,t,n)),t={cache:la()},e.payload=t;return}t=t.return}}function Ps(e,t,n){var r=gu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Rs(e)?zs(t,n):(n=ii(e,t,n,r),n!==null&&(vu(n,e,r),Bs(n,t,r)))}function Fs(e,t,n){Is(e,t,n,gu())}function Is(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Rs(e))zs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Tr(s,o))return ri(e,t,i,0),G===null&&ni(),!1}catch{}if(n=ii(e,t,i,r),n!==null)return vu(n,e,r),Bs(n,t,r),!0}return!1}function Ls(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Rs(e)){if(t)throw Error(i(479))}else t=ii(e,n,r,2),t!==null&&vu(t,e,2)}function Rs(e){var t=e.alternate;return e===B||t!==null&&t===B}function zs(e,t){_o=go=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Bs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,nt(e,n)}}var Vs={readContext:na,use:Io,useCallback:Co,useContext:Co,useEffect:Co,useImperativeHandle:Co,useLayoutEffect:Co,useInsertionEffect:Co,useMemo:Co,useReducer:Co,useRef:Co,useState:Co,useDebugValue:Co,useDeferredValue:Co,useTransition:Co,useSyncExternalStore:Co,useId:Co,useHostTransitionStatus:Co,useFormState:Co,useActionState:Co,useOptimistic:Co,useMemoCache:Co,useCacheRefresh:Co};Vs.useEffectEvent=Co;var Hs={readContext:na,use:Io,useCallback:function(e,t){return Mo().memoizedState=[e,t===void 0?null:t],e},useContext:na,useEffect:fs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),us(4194308,4,vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return us(4194308,4,e,t)},useInsertionEffect:function(e,t){us(4,2,e,t)},useMemo:function(e,t){var n=Mo();t=t===void 0?null:t;var r=e();if(vo){ze(!0);try{e()}finally{ze(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Mo();if(n!==void 0){var i=n(t);if(vo){ze(!0);try{n(t)}finally{ze(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ps.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Mo();return e={current:e},t.memoizedState=e},useState:function(e){e=Jo(e);var t=e.queue,n=Fs.bind(null,B,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:bs,useDeferredValue:function(e,t){return Cs(Mo(),e,t)},useTransition:function(){var e=Jo(!1);return e=Ts.bind(null,B,e.queue,!0,!1),Mo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=B,a=Mo();if(R){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),G===null)throw Error(i(349));q&127||Uo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,fs(Go.bind(null,r,o,e),[e]),r.flags|=2048,cs(9,{destroy:void 0},Wo.bind(null,r,o,n,t),null),n},useId:function(){var e=Mo(),t=G.identifierPrefix;if(R){var n=Oi,r=Di;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=yo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[lt]=t,o[ut]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Id(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Fc(t)}}return Rc(t),H(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Fc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ue.current,Hi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Pi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[lt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Nd(e.nodeValue,n)),e||zi(t,!0)}else e=Hd(e).createTextNode(r),e[lt]=t,t.stateNode=e}return Rc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Hi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[lt]=t}else Ui(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Rc(t),e=!1}else n=Wi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(lo(t),t):(lo(t),null);if(t.flags&128)throw Error(i(558))}return Rc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Hi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[lt]=t}else Ui(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Rc(t),a=!1}else a=Wi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(lo(t),t):(lo(t),null)}return lo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),U(t,t.updateQueue),Rc(t),null);case 4:return pe(),e===null&&Cd(t.stateNode.containerInfo),Rc(t),null;case 10:return Xi(t.type),Rc(t),null;case 19:if(se(uo),r=t.memoizedState,r===null)return Rc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Lc(r,!1);else{if(ql!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=fo(e),o!==null){for(t.flags|=128,Lc(r,!1),e=o.updateQueue,t.updateQueue=e,U(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)fi(n,e),n=n.sibling;return F(uo,uo.current&1|2),R&&ki(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Oe()>iu&&(t.flags|=128,a=!0,Lc(r,!1),t.lanes=4194304)}else{if(!a)if(e=fo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,U(t,e),Lc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!R)return Rc(t),null}else 2*Oe()-r.renderingStartTime>iu&&n!==536870912&&(t.flags|=128,a=!0,Lc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Rc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Oe(),e.sibling=null,n=uo.current,F(uo,a?n&1|2:n&1),R&&ki(t,r.treeForkCount),e);case 22:case 23:return lo(t),no(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Rc(t),t.subtreeFlags&6&&(t.flags|=8192)):Rc(t),n=t.updateQueue,n!==null&&U(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&se(ya),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Xi(ca),Rc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(Mi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Xi(ca),pe(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return he(t),null;case 31:if(t.memoizedState!==null){if(lo(t),t.alternate===null)throw Error(i(340));Ui()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(lo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ui()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return se(uo),null;case 4:return pe(),null;case 10:return Xi(t.type),null;case 22:case 23:return lo(t),no(),e!==null&&se(ya),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Xi(ca),null;case 25:return null;default:return null}}function Vc(e,t){switch(Mi(t),t.tag){case 3:Xi(ca),pe();break;case 26:case 27:case 5:he(t);break;case 4:pe();break;case 31:t.memoizedState!==null&&lo(t);break;case 13:lo(t);break;case 19:se(uo);break;case 10:Xi(t.type);break;case 22:case 23:lo(t),no(),e!==null&&se(ya);break;case 24:Xi(ca)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){J(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){J(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){J(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Qa(t,n)}catch(t){J(e,e.return,t)}}}function Gc(e,t,n){n.props=Ys(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){J(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){J(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){J(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){J(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){J(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Ld(r,e.type,n,t),r[ut]=t}catch(t){J(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Qd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Qd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=nn));else if(r!==4&&(r===27&&Qd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Qd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Id(t,r,n),t[lt]=e,t[ut]=n}catch(t){J(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Bd=cp,e=Ar(e),jr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Vd={focusedElem:e,selectionRange:n},cp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Id(o,r,n),o[lt]=e,St(o),r=o;break a;case`link`:var s=Hf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Or(s,h),v=Or(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,N.T=null,n=fu,fu=null;var o=cu,s=uu;if(su=0,lu=cu=null,uu=0,W&6)throw Error(i(331));var c=W;if(W|=4,Il(o.current),Ol(o,o.current,s,n),W=c,Y(0,!1),Re&&typeof Re.onPostCommitFiberRoot==`function`)try{Re.onPostCommitFiberRoot(Le,o)}catch{}return!0}finally{P.p=a,N.T=r,Wu(e,t)}}function qu(e,t,n){t=yi(n,t),t=ec(e.stateNode,t,2),e=Ga(e,t,2),e!==null&&($e(e,2),od(e))}function J(e,t,n){if(e.tag===3)qu(e,e,n);else for(;t!==null;){if(t.tag===3){qu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ou===null||!ou.has(r))){e=yi(n,e),n=tc(2),r=Ga(t,n,2),r!==null&&(nc(n,r,t,e),$e(r,2),od(r));break}}t=t.return}}function Ju(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Gl=!0,i.add(n),e=Yu.bind(null,e,t,n),t.then(e,e))}function Yu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,G===e&&(q&n)===n&&(ql===4||ql===3&&(q&62914560)===q&&300>Oe()-nu?!(W&2)&&Tu(e,0):Xl|=n,Ql===q&&(Ql=0)),od(e)}function Xu(e,t){t===0&&(t=Ze()),e=ai(e,t),e!==null&&($e(e,t),od(e))}function Zu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Xu(e,n)}function Qu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Xu(e,n)}function $u(e,t){return we(e,t)}var ed=null,td=null,nd=!1,rd=!1,id=!1,ad=0;function od(e){e!==td&&e.next===null&&(td===null?ed=td=e:td=td.next=e),rd=!0,nd||(nd=!0,fd())}function Y(e,t){if(!id&&rd){id=!0;do for(var n=!1,r=ed;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Be(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,dd(r,a))}else a=q,a=Je(r,r===G?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ye(r,a)||(n=!0,dd(r,a));r=r.next}while(n);id=!1}}function sd(){cd()}function cd(){rd=nd=!1;var e=0;ad!==0&&qd()&&(e=ad);for(var t=Oe(),n=null,r=ed;r!==null;){var i=r.next,a=ld(r,t);a===0?(r.next=null,n===null?ed=i:n.next=i,i===null&&(td=n)):(n=r,(e!==0||a&3)&&(rd=!0)),r=i}su!==0&&su!==5||Y(e,!1),ad!==0&&(ad=0)}function ld(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Rd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Sf(e,t,n){var r=xf;if(r&&typeof t==`string`&&t){var i=Vt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),gf.has(i)||(gf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Id(t,`link`,e),St(t),r.head.appendChild(t)))}}function Cf(e){vf.D(e),Sf(`dns-prefetch`,e,null)}function wf(e,t){vf.C(e,t),Sf(`preconnect`,e,t)}function Tf(e,t,n){vf.L(e,t,n);var r=xf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Vt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Vt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Vt(n.imageSizes)+`"]`)):i+=`[href="`+Vt(e)+`"]`;var a=i;switch(t){case`style`:a=jf(e);break;case`script`:a=Ff(e)}hf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),hf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Mf(a))||t===`script`&&r.querySelector(If(a))||(t=r.createElement(`link`),Id(t,`link`,e),St(t),r.head.appendChild(t)))}}function Ef(e,t){vf.m(e,t);var n=xf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Vt(r)+`"][href="`+Vt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Ff(e)}if(!hf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),hf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(If(a)))return}r=n.createElement(`link`),Id(r,`link`,e),St(r),n.head.appendChild(r)}}}function Df(e,t,n){vf.S(e,t,n);var r=xf;if(r&&e){var i=xt(r).hoistableStyles,a=jf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Mf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=hf.get(a))&&zf(e,n);var c=o=r.createElement(`link`);St(c),Id(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Rf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Of(e,t){vf.X(e,t);var n=xf;if(n&&e){var r=xt(n).hoistableScripts,i=Ff(e),a=r.get(i);a||(a=n.querySelector(If(i)),a||(e=m({src:e,async:!0},t),(t=hf.get(i))&&Bf(e,t),a=n.createElement(`script`),St(a),Id(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t){vf.M(e,t);var n=xf;if(n&&e){var r=xt(n).hoistableScripts,i=Ff(e),a=r.get(i);a||(a=n.querySelector(If(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=hf.get(i))&&Bf(e,t),a=n.createElement(`script`),St(a),Id(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Af(e,t,n,r){var a=(a=ue.current)?_f(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=jf(n.href),n=xt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=jf(n.href);var o=xt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Mf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),hf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},hf.set(e,n),o||Pf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Ff(n),n=xt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function jf(e){return`href="`+Vt(e)+`"`}function Mf(e){return`link[rel="stylesheet"][`+e+`]`}function Nf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Pf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Id(t,`link`,n),St(t),e.head.appendChild(t))}function Ff(e){return`[src="`+Vt(e)+`"]`}function If(e){return`script[async]`+e}function Lf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Vt(n.href)+`"]`);if(r)return t.instance=r,St(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),St(r),Id(r,`style`,a),Rf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=jf(n.href);var o=e.querySelector(Mf(a));if(o)return t.state.loading|=4,t.instance=o,St(o),o;r=Nf(n),(a=hf.get(a))&&zf(r,a),o=(e.ownerDocument||e).createElement(`link`),St(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Id(o,`link`,r),t.state.loading|=4,Rf(o,n.precedence,e),t.instance=o;case`script`:return o=Ff(n.src),(a=e.querySelector(If(o)))?(t.instance=a,St(a),a):(r=n,(a=hf.get(o))&&(r=m({},n),Bf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),St(a),Id(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Rf(r,n.precedence,e));return t.instance}function Rf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Wf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Gf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Kf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=jf(r.href),a=t.querySelector(Mf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Yf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,St(a);return}a=t.ownerDocument||t,r=Nf(r),(i=hf.get(i))&&zf(r,i),a=a.createElement(`link`),St(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Id(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Yf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var qf=0;function Jf(e,t){return e.stylesheets&&e.count===0&&Zf(e,e.stylesheets),0qf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Yf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Zf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Xf=null;function Zf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Xf=new Map,t.forEach(Qf,e),Xf=null,Yf.call(e))}function Qf(e,t){if(!(t.state.loading&4)){var n=Xf.get(e);if(n)var r=n.get(null);else{n=new Map,Xf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=o(((e,t)=>{var n=typeof Element<`u`,r=typeof Map==`function`,i=typeof Set==`function`,a=typeof ArrayBuffer==`function`&&!!ArrayBuffer.isView;function o(e,t){if(e===t)return!0;if(e&&t&&typeof e==`object`&&typeof t==`object`){if(e.constructor!==t.constructor)return!1;var s,c,l;if(Array.isArray(e)){if(s=e.length,s!=t.length)return!1;for(c=s;c--!==0;)if(!o(e[c],t[c]))return!1;return!0}var u;if(r&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(u=e.entries();!(c=u.next()).done;)if(!t.has(c.value[0]))return!1;for(u=e.entries();!(c=u.next()).done;)if(!o(c.value[1],t.get(c.value[0])))return!1;return!0}if(i&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(u=e.entries();!(c=u.next()).done;)if(!t.has(c.value[0]))return!1;return!0}if(a&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(s=e.length,s!=t.length)return!1;for(c=s;c--!==0;)if(e[c]!==t[c])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf&&typeof e.valueOf==`function`&&typeof t.valueOf==`function`)return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString&&typeof e.toString==`function`&&typeof t.toString==`function`)return e.toString()===t.toString();if(l=Object.keys(e),s=l.length,s!==Object.keys(t).length)return!1;for(c=s;c--!==0;)if(!Object.prototype.hasOwnProperty.call(t,l[c]))return!1;if(n&&e instanceof Element)return!1;for(c=s;c--!==0;)if(!((l[c]===`_owner`||l[c]===`__v`||l[c]===`__o`)&&e.$$typeof)&&!o(e[l[c]],t[l[c]]))return!1;return!0}return e!==e&&t!==t}t.exports=function(e,t){try{return o(e,t)}catch(e){if((e.message||``).match(/stack|recursion/i))return console.warn(`react-fast-compare cannot handle circular refs`),!1;throw e}}})),y=o(((e,t)=>{t.exports=function(e,t,n,r,i,a,o,s){if(!e){var c;if(t===void 0)c=Error(`Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.`);else{var l=[n,r,i,a,o,s],u=0;c=Error(t.replace(/%s/g,function(){return l[u++]})),c.name=`Invariant Violation`}throw c.framesToPop=1,c}}})),b=o(((e,t)=>{t.exports=function(e,t,n,r){var i=n?n.call(r,e,t):void 0;if(i!==void 0)return!!i;if(e===t)return!0;if(typeof e!=`object`||!e||typeof t!=`object`||!t)return!1;var a=Object.keys(e),o=Object.keys(t);if(a.length!==o.length)return!1;for(var s=Object.prototype.hasOwnProperty.bind(t),c=0;c(e.BASE=`base`,e.BODY=`body`,e.HEAD=`head`,e.HTML=`html`,e.LINK=`link`,e.META=`meta`,e.NOSCRIPT=`noscript`,e.SCRIPT=`script`,e.STYLE=`style`,e.TITLE=`title`,e.FRAGMENT=`Symbol(react.fragment)`,e))(E||{}),ee={link:{rel:[`amphtml`,`canonical`,`alternate`]},script:{type:[`application/ld+json`]},meta:{charset:``,name:[`generator`,`robots`,`description`],property:[`og:type`,`og:title`,`og:url`,`og:image`,`og:image:alt`,`og:description`,`twitter:url`,`twitter:title`,`twitter:description`,`twitter:image`,`twitter:image:alt`,`twitter:card`,`twitter:site`]}},D=Object.values(E),O={accesskey:`accessKey`,charset:`charSet`,class:`className`,contenteditable:`contentEditable`,contextmenu:`contextMenu`,"http-equiv":`httpEquiv`,itemprop:`itemProp`,tabindex:`tabIndex`},te=Object.entries(O).reduce((e,[t,n])=>(e[n]=t,e),{}),k=`data-rh`,A={DEFAULT_TITLE:`defaultTitle`,DEFER:`defer`,ENCODE_SPECIAL_CHARACTERS:`encodeSpecialCharacters`,ON_CHANGE_CLIENT_STATE:`onChangeClientState`,TITLE_TEMPLATE:`titleTemplate`,PRIORITIZE_SEO_TAGS:`prioritizeSeoTags`},j=(e,t)=>{for(let n=e.length-1;n>=0;--n){let r=e[n];if(Object.prototype.hasOwnProperty.call(r,t))return r[t]}return null},ne=e=>{let t=j(e,`title`),n=j(e,A.TITLE_TEMPLATE);if(Array.isArray(t)&&(t=t.join(``)),n&&t)return n.replace(/%s/g,()=>t);let r=j(e,A.DEFAULT_TITLE);return t||r||void 0},M=e=>j(e,A.ON_CHANGE_CLIENT_STATE)||(()=>{}),N=(e,t)=>t.filter(t=>t[e]!==void 0).map(t=>t[e]).reduce((e,t)=>({...e,...t}),{}),P=(e,t)=>t.filter(e=>e.base!==void 0).map(e=>e.base).reverse().reduce((t,n)=>{if(!t.length){let r=Object.keys(n);for(let i=0;iconsole&&typeof console.warn==`function`&&console.warn(e),ie=(e,t,n)=>{let r={};return n.filter(t=>Array.isArray(t[e])?!0:(t[e]!==void 0&&re(`Helmet: ${e} should be of type "Array". Instead found type "${typeof t[e]}"`),!1)).map(t=>t[e]).reverse().reduce((e,n)=>{let i={};n.filter(e=>{let n,a=Object.keys(e);for(let r=0;re.push(t));let a=Object.keys(i);for(let e=0;e{if(Array.isArray(e)&&e.length){for(let n=0;n({baseTag:P([`href`],e),bodyAttributes:N(`bodyAttributes`,e),defer:j(e,A.DEFER),encode:j(e,A.ENCODE_SPECIAL_CHARACTERS),htmlAttributes:N(`htmlAttributes`,e),linkTags:ie(`link`,[`rel`,`href`],e),metaTags:ie(`meta`,[`name`,`charset`,`http-equiv`,`property`,`itemprop`],e),noscriptTags:ie(`noscript`,[`innerHTML`],e),onChangeClientState:M(e),scriptTags:ie(`script`,[`src`,`innerHTML`],e),styleTags:ie(`style`,[`cssText`],e),title:ne(e),titleAttributes:N(`titleAttributes`,e),prioritizeSeoTags:ae(e,A.PRIORITIZE_SEO_TAGS)}),se=e=>Array.isArray(e)?e.join(``):e,F=(e,t)=>{let n=Object.keys(e);for(let r=0;rArray.isArray(e)?e.reduce((e,n)=>(F(n,t)?e.priority.push(n):e.default.push(n),e),{priority:[],default:[]}):{default:e,priority:[]},le=(e,t)=>({...e,[t]:void 0}),ue=[`noscript`,`script`,`style`],de=(e,t=!0)=>t===!1?String(e):String(e).replace(/&/g,`&`).replace(//g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),fe=e=>Object.keys(e).reduce((t,n)=>{let r=e[n]===void 0?`${n}`:`${n}="${e[n]}"`;return t?`${t} ${r}`:r},``),pe=(e,t,n,r)=>{let i=fe(n),a=se(t);return i?`<${e} ${k}="true" ${i}>${de(a,r)}`:`<${e} ${k}="true">${de(a,r)}`},me=(e,t,n=!0)=>t.reduce((t,r)=>{let i=r,a=Object.keys(i).filter(e=>!(e===`innerHTML`||e===`cssText`)).reduce((e,t)=>{let r=i[t]===void 0?t:`${t}="${de(i[t],n)}"`;return e?`${e} ${r}`:r},``),o=i.innerHTML||i.cssText||``;return`${t}<${e} ${k}="true" ${a}${ue.indexOf(e)===-1?`/>`:`>${o}`}`},``),he=(e,t={})=>Object.keys(e).reduce((t,n)=>{let r=O[n];return t[r||n]=e[n],t},t),ge=(e,t,n)=>{let r=he(n,{key:t,[k]:!0});return[S.createElement(`title`,r,t)]},_e=(e,t)=>t.map((t,n)=>{let r={key:n,[k]:!0};return Object.keys(t).forEach(e=>{let n=O[e]||e;n===`innerHTML`||n===`cssText`?r.dangerouslySetInnerHTML={__html:t.innerHTML||t.cssText}:r[n]=t[e]}),S.createElement(e,r)}),ve=(e,t,n=!0)=>{switch(e){case`title`:return{toComponent:()=>ge(e,t.title,t.titleAttributes),toString:()=>pe(e,t.title,t.titleAttributes,n)};case`bodyAttributes`:case`htmlAttributes`:return{toComponent:()=>he(t),toString:()=>fe(t)};default:return{toComponent:()=>_e(e,t),toString:()=>me(e,t,n)}}},ye=({metaTags:e,linkTags:t,scriptTags:n,encode:r})=>{let i=ce(e,ee.meta),a=ce(t,ee.link),o=ce(n,ee.script);return{priorityMethods:{toComponent:()=>[..._e(`meta`,i.priority),..._e(`link`,a.priority),..._e(`script`,o.priority)],toString:()=>`${ve(`meta`,i.priority,r)} ${ve(`link`,a.priority,r)} ${ve(`script`,o.priority,r)}`},metaTags:i.default,linkTags:a.default,scriptTags:o.default}},be=e=>{let{baseTag:t,bodyAttributes:n,encode:r=!0,htmlAttributes:i,noscriptTags:a,styleTags:o,title:s=``,titleAttributes:c,prioritizeSeoTags:l}=e,{linkTags:u,metaTags:d,scriptTags:f}=e,p={toComponent:()=>[],toString:()=>``};return l&&({priorityMethods:p,linkTags:u,metaTags:d,scriptTags:f}=ye(e)),{priority:p,base:ve(`base`,t,r),bodyAttributes:ve(`bodyAttributes`,n,r),htmlAttributes:ve(`htmlAttributes`,i,r),link:ve(`link`,u,r),meta:ve(`meta`,d,r),noscript:ve(`noscript`,a,r),script:ve(`script`,f,r),style:ve(`style`,o,r),title:ve(`title`,{title:s,titleAttributes:c},r)}},xe=[],Se=!!(typeof window<`u`&&window.document&&window.document.createElement),Ce=class{instances=[];canUseDOM=Se;context;value={setHelmet:e=>{this.context.helmet=e},helmetInstances:{get:()=>this.canUseDOM?xe:this.instances,add:e=>{(this.canUseDOM?xe:this.instances).push(e)},remove:e=>{let t=(this.canUseDOM?xe:this.instances).indexOf(e);(this.canUseDOM?xe:this.instances).splice(t,1)}}};constructor(e,t){this.context=e,this.canUseDOM=t||!1,t||(e.helmet=be({baseTag:[],bodyAttributes:{},encodeSpecialCharacters:!0,htmlAttributes:{},linkTags:[],metaTags:[],noscriptTags:[],scriptTags:[],styleTags:[],title:``,titleAttributes:{}}))}},we=parseInt(`19.2.0`.split(`.`)[0],10)>=19,Te=S.createContext({}),Ee=class e extends S.Component{static canUseDOM=Se;helmetData;constructor(t){super(t),we?this.helmetData=null:this.helmetData=new Ce(this.props.context||{},e.canUseDOM)}render(){return we?S.createElement(S.Fragment,null,this.props.children):S.createElement(Te.Provider,{value:this.helmetData.value},this.props.children)}},De=(e,t)=>{let n=document.head||document.querySelector(`head`),r=n.querySelectorAll(`${e}[${k}]`),i=[].slice.call(r),a=[],o;return t&&t.length&&t.forEach(t=>{let n=document.createElement(e);for(let e in t)if(Object.prototype.hasOwnProperty.call(t,e))if(e===`innerHTML`)n.innerHTML=t.innerHTML;else if(e===`cssText`){let e=t.cssText;n.appendChild(document.createTextNode(e))}else{let r=e,i=t[r]===void 0?``:t[r];n.setAttribute(e,i)}n.setAttribute(k,`true`),i.some((e,t)=>(o=t,n.isEqualNode(e)))?i.splice(o,1):a.push(n)}),i.forEach(e=>e.parentNode?.removeChild(e)),a.forEach(e=>n.appendChild(e)),{oldTags:i,newTags:a}},Oe=(e,t)=>{let n=document.getElementsByTagName(e)[0];if(!n)return;let r=n.getAttribute(k),i=r?r.split(`,`):[],a=[...i],o=Object.keys(t);for(let e of o){let r=t[e]||``;n.getAttribute(e)!==r&&n.setAttribute(e,r),i.indexOf(e)===-1&&i.push(e);let o=a.indexOf(e);o!==-1&&a.splice(o,1)}for(let e=a.length-1;e>=0;--e)n.removeAttribute(a[e]);i.length===a.length?n.removeAttribute(k):n.getAttribute(k)!==o.join(`,`)&&n.setAttribute(k,o.join(`,`))},ke=(e,t)=>{e!==void 0&&document.title!==e&&(document.title=se(e)),Oe(`title`,t)},Ae=(e,t)=>{let{baseTag:n,bodyAttributes:r,htmlAttributes:i,linkTags:a,metaTags:o,noscriptTags:s,onChangeClientState:c,scriptTags:l,styleTags:u,title:d,titleAttributes:f}=e;Oe(`body`,r),Oe(`html`,i),ke(d,f);let p={baseTag:De(`base`,n),linkTags:De(`link`,a),metaTags:De(`meta`,o),noscriptTags:De(`noscript`,s),scriptTags:De(`script`,l),styleTags:De(`style`,u)},m={},h={};Object.keys(p).forEach(e=>{let{newTags:t,oldTags:n}=p[e];t.length&&(m[e]=t),n.length&&(h[e]=p[e].oldTags)}),t&&t(),c(e,m,h)},je=null,Me=e=>{je&&cancelAnimationFrame(je),e.defer?je=requestAnimationFrame(()=>{Ae(e,()=>{je=null})}):(Ae(e),je=null)},Ne=class extends S.Component{rendered=!1;shouldComponentUpdate(e){return!(0,T.default)(e,this.props)}componentDidUpdate(){this.emitChange()}componentWillUnmount(){let{helmetInstances:e}=this.props.context;e.remove(this),this.emitChange()}emitChange(){let{helmetInstances:e,setHelmet:t}=this.props.context,n=null,r=oe(e.get().map(e=>{let{context:t,...n}=e.props;return n}));Ee.canUseDOM?Me(r):be&&(n=be(r)),t(n)}init(){if(this.rendered)return;this.rendered=!0;let{helmetInstances:e}=this.props.context;e.add(this),this.emitChange()}render(){return this.init(),null}},Pe=[],Fe=e=>{let t={};for(let n of Object.keys(e))t[te[n]||n]=e[n];return t},Ie=e=>{let t={};for(let n of Object.keys(e)){let r=O[n];t[r||n]=e[n]}return t},Le=(e,t)=>{if(!Se)return;let n=document.getElementsByTagName(e)[0];if(!n)return;let r=`data-rh-managed`,i=n.getAttribute(r),a=i?i.split(`,`):[],o=Object.keys(t);for(let e of a)o.includes(e)||n.removeAttribute(e);for(let e of o){let r=t[e];r==null||r===!1?n.removeAttribute(e):r===!0?n.setAttribute(e,``):n.setAttribute(e,String(r))}o.length>0?n.setAttribute(r,o.join(`,`)):n.removeAttribute(r)},Re=()=>{let e={},t={};for(let n of Pe){let{htmlAttributes:r,bodyAttributes:i}=n.props;r&&Object.assign(e,Fe(r)),i&&Object.assign(t,Fe(i))}Le(`html`,e),Le(`body`,t)},ze=class extends S.Component{componentDidMount(){Pe.push(this),Re()}componentDidUpdate(){Re()}componentWillUnmount(){let e=Pe.indexOf(this);e!==-1&&Pe.splice(e,1),Re()}resolveTitle(){let{title:e,titleTemplate:t,defaultTitle:n}=this.props;return e&&t?t.replace(/%s/g,()=>Array.isArray(e)?e.join(``):e):e||n||void 0}renderTitle(){let e=this.resolveTitle();if(e===void 0)return null;let t=this.props.titleAttributes||{};return S.createElement(`title`,Ie(t),e)}renderBase(){let{base:e}=this.props;return e?S.createElement(`base`,Ie(e)):null}renderMeta(){let{meta:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>S.createElement(`meta`,{key:t,...Ie(e)}))}renderLink(){let{link:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>S.createElement(`link`,{key:t,...Ie(e)}))}renderScript(){let{script:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>{let{innerHTML:n,...r}=e,i=Ie(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),S.createElement(`script`,{key:t,...i})})}renderStyle(){let{style:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>{let{cssText:n,...r}=e,i=Ie(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),S.createElement(`style`,{key:t,...i})})}renderNoscript(){let{noscript:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>{let{innerHTML:n,...r}=e,i=Ie(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),S.createElement(`noscript`,{key:t,...i})})}render(){return S.createElement(S.Fragment,null,this.renderTitle(),this.renderBase(),this.renderMeta(),this.renderLink(),this.renderScript(),this.renderStyle(),this.renderNoscript())}},Be=class extends S.Component{static defaultProps={defer:!0,encodeSpecialCharacters:!0,prioritizeSeoTags:!1};shouldComponentUpdate(e){return!(0,C.default)(le(this.props,`helmetData`),le(e,`helmetData`))}mapNestedChildrenToProps(e,t){if(!t)return null;switch(e.type){case`script`:case`noscript`:return{innerHTML:t};case`style`:return{cssText:t};default:throw Error(`<${e.type} /> elements are self-closing and can not contain children. Refer to our API for more information.`)}}flattenArrayTypeChildren(e,t,n,r){return{...t,[e.type]:[...t[e.type]||[],{...n,...this.mapNestedChildrenToProps(e,r)}]}}mapObjectTypeChildren(e,t,n,r){switch(e.type){case`title`:return{...t,[e.type]:r,titleAttributes:{...n}};case`body`:return{...t,bodyAttributes:{...n}};case`html`:return{...t,htmlAttributes:{...n}};default:return{...t,[e.type]:{...n}}}}mapArrayTypeChildrenToProps(e,t){let n={...t};return Object.keys(e).forEach(t=>{n={...n,[t]:e[t]}}),n}warnOnInvalidChildren(e,t){return(0,w.default)(D.some(t=>e.type===t),typeof e.type==`function`?`You may be attempting to nest components within each other, which is not allowed. Refer to our API for more information.`:`Only elements types ${D.join(`, `)} are allowed. Helmet does not support rendering <${e.type}> elements. Refer to our API for more information.`),(0,w.default)(!t||typeof t==`string`||Array.isArray(t)&&!t.some(e=>typeof e!=`string`),`Helmet expects a string as a child of <${e.type}>. Did you forget to wrap your children in braces? ( <${e.type}>{\`\`} ) Refer to our API for more information.`),!0}mapChildrenToProps(e,t){let n={};return S.Children.forEach(e,e=>{if(!e||!e.props)return;let{children:r,...i}=e.props,a=Object.keys(i).reduce((e,t)=>(e[te[t]||t]=i[t],e),{}),{type:o}=e;switch(typeof o==`symbol`?o=o.toString():this.warnOnInvalidChildren(e,r),o){case`Symbol(react.fragment)`:t=this.mapChildrenToProps(r,t);break;case`link`:case`meta`:case`noscript`:case`script`:case`style`:n=this.flattenArrayTypeChildren(e,n,a,r);break;default:t=this.mapObjectTypeChildren(e,t,a,r);break}}),this.mapArrayTypeChildrenToProps(n,t)}render(){let{children:e,...t}=this.props,n={...t},{helmetData:r}=t;return e&&(n=this.mapChildrenToProps(e,n)),r&&!(r instanceof Ce)&&(r=new Ce(r.context,!0),delete n.helmetData),we?S.createElement(ze,{...n}):r?S.createElement(Ne,{...n,context:r.value}):S.createElement(Te.Consumer,null,e=>S.createElement(Ne,{...n,context:e}))}},Ve=`modulepreload`,He=function(e){return`/_frontend/`+e},Ue={};const We=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=He(t,n),t in Ue)return;Ue[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:Ve,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};var Ge=`popstate`;function Ke(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function qe(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return $e(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:et(t)}return nt(t,n,null,e)}function Je(e={}){function t(e,t){let{pathname:n=`/`,search:r=``,hash:i=``}=tt(e.location.hash.substring(1));return!n.startsWith(`/`)&&!n.startsWith(`.`)&&(n=`/`+n),$e(``,{pathname:n,search:r,hash:i},t.state&&t.state.usr||null,t.state&&t.state.key||`default`)}function n(e,t){let n=e.document.querySelector(`base`),r=``;if(n&&n.getAttribute(`href`)){let t=e.location.href,n=t.indexOf(`#`);r=n===-1?t:t.slice(0,n)}return r+`#`+(typeof t==`string`?t:et(t))}function r(e,t){Xe(e.pathname.charAt(0)===`/`,`relative pathnames are not supported in hash history.push(${JSON.stringify(t)})`)}return nt(t,n,r,e)}function Ye(e,t){if(e===!1||e==null)throw Error(t)}function Xe(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function Ze(){return Math.random().toString(36).substring(2,10)}function Qe(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function $e(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?tt(t):t,state:n,key:t&&t.key||r||Ze(),mask:i}}function et({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function tt(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function nt(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=Ke(e)?e:$e(h.location,e,t);n&&n(r,e),l=u()+1;let d=Qe(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=Ke(e)?e:$e(h.location,e,t);n&&n(r,e),l=u();let i=Qe(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return rt(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(Ge,d),c=e,()=>{i.removeEventListener(Ge,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function rt(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),Ye(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:et(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function it(e,t,n=`/`){return at(e,t,n,!1)}function at(e,t,n,r,i){let a=wt((typeof t==`string`?tt(t):t).pathname||`/`,n);if(a==null)return null;let o=i??st(e),s=null,c=Ct(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;Ye(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Nt([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(Ye(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),ct(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:vt(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of lt(e.path))a(e,t,!0,n)}),t}function lt(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=lt(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function ut(e){e.sort((e,t)=>e.score===t.score?yt(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var dt=/^:[\w-]+$/,ft=3,pt=2,mt=1,ht=10,gt=-2,_t=e=>e===`*`;function vt(e,t){let n=e.split(`/`),r=n.length;return n.some(_t)&&(r+=gt),t&&(r+=pt),n.filter(e=>!_t(e)).reduce((e,t)=>e+(dt.test(t)?ft:t===``?mt:ht),r)}function yt(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function bt(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function St(e,t=!1,n=!0){Xe(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function Ct(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return Xe(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function wt(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var Tt=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Et(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?tt(e):e,a;return n?(n=Mt(n),a=n.startsWith(`/`)?Dt(n.substring(1),`/`):Dt(n,t)):a=t,{pathname:a,search:It(r),hash:Lt(i)}}function Dt(e,t){let n=Pt(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Ot(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function kt(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function At(e){let t=kt(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function jt(e,t,n,r=!1){let i;typeof e==`string`?i=tt(e):(i={...e},Ye(!i.pathname||!i.pathname.includes(`?`),Ot(`?`,`pathname`,`search`,i)),Ye(!i.pathname||!i.pathname.includes(`#`),Ot(`#`,`pathname`,`hash`,i)),Ye(!i.search||!i.search.includes(`#`),Ot(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Et(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Mt=e=>e.replace(/\/\/+/g,`/`),Nt=e=>Mt(e.join(`/`)),Pt=e=>e.replace(/\/+$/,``),Ft=e=>Pt(e).replace(/^\/*/,`/`),It=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Lt=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Rt=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function zt(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Bt(e){return Nt(e.map(e=>e.route.path).filter(Boolean))||`/`}var Vt=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ht(e,t){let n=e;if(typeof n!=`string`||!Tt.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Vt)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=wt(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{Xe(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ut=S.createContext(null);Ut.displayName=`DataRouter`;var Wt=S.createContext(null);Wt.displayName=`DataRouterState`;var Gt=S.createContext(!1);function Kt(){return S.useContext(Gt)}var qt=S.createContext({isTransitioning:!1});qt.displayName=`ViewTransition`;var Jt=S.createContext(new Map);Jt.displayName=`Fetchers`;var Yt=S.createContext(null);Yt.displayName=`Await`;var Xt=S.createContext(null);Xt.displayName=`Navigation`;var Zt=S.createContext(null);Zt.displayName=`Location`;var Qt=S.createContext({outlet:null,matches:[],isDataRoute:!1});Qt.displayName=`Route`;var $t=S.createContext(null);$t.displayName=`RouteError`;var en=`REACT_ROUTER_ERROR`,tn=`REDIRECT`,nn=`ROUTE_ERROR_RESPONSE`;function rn(e){if(e.startsWith(`${en}:${tn}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function an(e){if(e.startsWith(`${en}:${nn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Rt(t.status,t.statusText,t.data)}catch{}}function on(e,{relative:t}={}){Ye(sn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Xt),{hash:i,pathname:a,search:o}=pn(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Nt([n,a])),r.createHref({pathname:s,search:o,hash:i})}function sn(){return S.useContext(Zt)!=null}function cn(){return Ye(sn(),`useLocation() may be used only in the context of a component.`),S.useContext(Zt).location}var ln=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function un(e){S.useContext(Xt).static||S.useLayoutEffect(e)}function dn(){let{isDataRoute:e}=S.useContext(Qt);return e?jn():fn()}function fn(){Ye(sn(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(Ut),{basename:t,navigator:n}=S.useContext(Xt),{matches:r}=S.useContext(Qt),{pathname:i}=cn(),a=JSON.stringify(At(r)),o=S.useRef(!1);return un(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(Xe(o.current,ln),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=jt(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Nt([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}S.createContext(null);function pn(e,{relative:t}={}){let{matches:n}=S.useContext(Qt),{pathname:r}=cn(),i=JSON.stringify(At(n));return S.useMemo(()=>jt(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function mn(e,t,n){Ye(sn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Xt),{matches:i}=S.useContext(Qt),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Nn(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=cn(),d;if(t){let e=typeof t==`string`?tt(t):t;Ye(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):it(e,{pathname:p});Xe(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Xe(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=xn(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Nt([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Nt([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?S.createElement(Zt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function hn(){let e=An(),t=zt(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=S.createElement(S.Fragment,null,S.createElement(`p`,null,`💿 Hey developer 👋`),S.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,S.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,S.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),S.createElement(S.Fragment,null,S.createElement(`h2`,null,`Unexpected Application Error!`),S.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?S.createElement(`pre`,{style:i},n):null,o)}var gn=S.createElement(hn,null),_n=class extends S.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=an(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:S.createElement(Qt.Provider,{value:this.props.routeContext},S.createElement($t.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(yn,{error:e},t):t}};_n.contextType=Gt;var vn=new WeakMap;function yn({children:e,error:t}){let{basename:n}=S.useContext(Xt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=rn(t.digest);if(e){let r=vn.get(t);if(r)throw r;let i=Ht(e.location,n);if(Vt&&!vn.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vn.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function bn({routeContext:e,match:t,children:n}){let r=S.useContext(Ut);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(Qt.Provider,{value:e},n)}function xn(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);Ye(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Bt(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||gn,o&&(s<0&&c===0?(Nn(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?S.createElement(n.route.Component,null):n.route.element?n.route.element:e,S.createElement(bn,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(_n,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function Sn(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Cn(e){let t=S.useContext(Ut);return Ye(t,Sn(e)),t}function wn(e){let t=S.useContext(Wt);return Ye(t,Sn(e)),t}function Tn(e){let t=S.useContext(Qt);return Ye(t,Sn(e)),t}function En(e){let t=Tn(e),n=t.matches[t.matches.length-1];return Ye(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Dn(){return En(`useRouteId`)}function On(){return wn(`useNavigation`).navigation}function kn(){let{matches:e,loaderData:t}=wn(`useMatches`);return S.useMemo(()=>e.map(e=>ot(e,t)),[e,t])}function An(){let e=S.useContext($t),t=wn(`useRouteError`),n=En(`useRouteError`);return e===void 0?t.errors?.[n]:e}function jn(){let{router:e}=Cn(`useNavigate`),t=En(`useNavigate`),n=S.useRef(!1);return un(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{Xe(n.current,ln),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Mn={};function Nn(e,t,n){!t&&!Mn[e]&&(Mn[e]=!0,Xe(!1,n))}S.useOptimistic,S.memo(Pn);function Pn({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return mn(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Fn({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){Ye(!sn(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=S.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=tt(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=S.useMemo(()=>{let e=wt(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return Xe(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:S.createElement(Xt.Provider,{value:c},S.createElement(Zt.Provider,{children:t,value:h}))}var In=`get`,Ln=`application/x-www-form-urlencoded`;function Rn(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function zn(e){return Rn(e)&&e.tagName.toLowerCase()===`button`}function Bn(e){return Rn(e)&&e.tagName.toLowerCase()===`form`}function Vn(e){return Rn(e)&&e.tagName.toLowerCase()===`input`}function Hn(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Un(e,t){return e.button===0&&(!t||t===`_self`)&&!Hn(e)}var Wn=null;function Gn(){if(Wn===null)try{new FormData(document.createElement(`form`),0),Wn=!1}catch{Wn=!0}return Wn}var Kn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function qn(e){return e!=null&&!Kn.has(e)?(Xe(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Ln}"`),null):e}function Jn(e,t){let n,r,i,a,o;if(Bn(e)){let o=e.getAttribute(`action`);r=o?wt(o,t):null,n=e.getAttribute(`method`)||In,i=qn(e.getAttribute(`enctype`))||Ln,a=new FormData(e)}else if(zn(e)||Vn(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a