diff --git a/.gitignore b/.gitignore index e4a949c..a30ddc4 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,9 @@ CLAUDE.local.md .continue/ + +# Working backups created by the SSHFS-deploy workflow +*.bak.* + +# TYPO3 auto-generated public assets +/public/_assets_install/ diff --git a/README.md b/README.md index ee8d704..313885e 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,8 @@ $debugMode = $request->getAttribute('site')->getSettings()->get('vitec.debugMode # Custom Frames: -/usr/home/vitecevo/public_html/live/packages/vitec/Configuration/TCA/Overrides/sys_category.php \ No newline at end of file +/usr/home/vitecevo/public_html/live/packages/vitec/Configuration/TCA/Overrides/sys_category.php + + + +https://www.figma.com/design/0EZHIXmmRoIRk0BljKzhXq/VITEC?node-id=61-220&p=f \ No newline at end of file diff --git a/composer.json b/composer.json index 24c8111..2651f9a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "typo3/cms-base-distribution", - "description" : "TYPO3 CMS Base Distribution", + "description": "TYPO3 CMS Base Distribution", "license": "GPL-2.0-or-later", "type": "project", "config": { @@ -17,47 +17,44 @@ "require": { "b13/container": "^3.1", "b13/typo3-updater": "^1.1", - "co-stack/logs": "^5.3", "evomedien/vitec": "^1.0.1", - "friendsoftypo3/content-blocks": "^1.3", - "friendsoftypo3/headless": "^4.2", - "itplusx/headless-container": "^3.0", - "netzbewegung/nb-headless-content-blocks": "^0.0.21", - "nitsan/ns-license": "^13.0", - "typo3/cms-backend": "^13.4", - "typo3/cms-belog": "^13.4", - "typo3/cms-beuser": "^13.4", - "typo3/cms-core": "^13.4", - "typo3/cms-dashboard": "^13.4", - "typo3/cms-extbase": "^13.4", - "typo3/cms-extensionmanager": "^13.4", - "typo3/cms-felogin": "^13.4", - "typo3/cms-filelist": "^13.4", - "typo3/cms-fluid": "^13.4", - "typo3/cms-fluid-styled-content": "^13.4", - "typo3/cms-form": "^13.4", - "typo3/cms-frontend": "^13.4", - "typo3/cms-impexp": "^13.4", - "typo3/cms-info": "^13.4", - "typo3/cms-install": "^13.4", - "typo3/cms-reactions": "^13.4", - "typo3/cms-recycler": "^13.4", - "typo3/cms-rte-ckeditor": "^13.4", - "typo3/cms-scheduler": "^13.4", - "typo3/cms-seo": "^13.4", - "typo3/cms-setup": "^13.4", - "typo3/cms-sys-note": "^13.4", - "typo3/cms-tstemplate": "^13.4", - "typo3/cms-viewpage": "^13.4", - "typo3/cms-webhooks": "^13.4", - "w3code/w3c-categorymanager": "^13.4" + "friendsoftypo3/content-blocks": "^2.4", + "friendsoftypo3/headless": "^5.0@rc", + "netzbewegung/nb-headless-content-blocks": "^0.0.23", + "typo3/cms-backend": "^14.3", + "typo3/cms-belog": "^14.3", + "typo3/cms-beuser": "^14.3", + "typo3/cms-core": "^14.3", + "typo3/cms-dashboard": "^14.3", + "typo3/cms-extbase": "^14.3", + "typo3/cms-extensionmanager": "^14.3", + "typo3/cms-felogin": "^14.3", + "typo3/cms-filelist": "^14.3", + "typo3/cms-fluid": "^14.3", + "typo3/cms-fluid-styled-content": "^14.3", + "typo3/cms-form": "^14.3", + "typo3/cms-frontend": "^14.3", + "typo3/cms-impexp": "^14.3", + "typo3/cms-info": "^14.3", + "typo3/cms-install": "^14.3", + "typo3/cms-reactions": "^14.3", + "typo3/cms-recycler": "^14.3", + "typo3/cms-rte-ckeditor": "^14.3", + "typo3/cms-scheduler": "^14.3", + "typo3/cms-seo": "^14.3", + "typo3/cms-sys-note": "^14.3", + "typo3/cms-tstemplate": "^14.3", + "typo3/cms-viewpage": "^14.3", + "typo3/cms-webhooks": "^14.3" }, "repositories": [ { "name": "t3planet", "type": "composer", "url": "https://composer.t3planet.cloud", - "only": ["nitsan/ns_theme_reactbootstrap"] + "only": [ + "nitsan/ns_theme_reactbootstrap" + ] }, { "type": "path", diff --git a/composer.lock b/composer.lock index da1912c..e454b71 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": "c6c4d1d82c761620488ccd3a9f1e5789", + "content-hash": "1b855a4234befa546bf9280aa9536303", "packages": [ { "name": "b13/container", @@ -221,67 +221,6 @@ }, "time": "2021-02-26T10:19:33+00:00" }, - { - "name": "co-stack/logs", - "version": "5.3.1", - "source": { - "type": "git", - "url": "https://gitlab.com/co-stack.com/co-stack.com/typo3-extensions/logs.git", - "reference": "2512831eb330f9863717d8d9bd3fc51408c654a4" - }, - "dist": { - "type": "zip", - "url": "https://gitlab.com/api/v4/projects/co-stack%2Ecom%2Fco-stack%2Ecom%2Ftypo3-extensions%2Flogs/repository/archive.zip?sha=2512831eb330f9863717d8d9bd3fc51408c654a4", - "reference": "2512831eb330f9863717d8d9bd3fc51408c654a4", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pdo": "*", - "php": "^8.1", - "psr/log": "^2.0 || ^3.0", - "typo3/cms-core": "^12.4 || ^13.0" - }, - "require-dev": { - "co-stack/logs-dev": "@dev", - "typo3/cms-beuser": "^12.4 || ^13.4", - "typo3/cms-install": "^12.4 || ^13.4", - "typo3/cms-lowlevel": "^12.4 || ^13.4", - "typo3/minimal": "^12.4 || ^13.4", - "typo3/testing-framework": "^8.0 || ^9.0" - }, - "type": "typo3-cms-extension", - "extra": { - "typo3/cms": { - "web-dir": ".project/typo3/public", - "extension-key": "logs" - }, - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "CoStack\\Logs\\": "Classes/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Oliver Eglseder", - "email": "oliver.eglseder@co-stack.com" - } - ], - "description": "TYPO3 Logging API reading module and devlog extension in one", - "support": { - "issues": "https://gitlab.com/co-stack.com/co-stack.com/typo3-extensions/logs/-/issues", - "source": "https://gitlab.com/co-stack.com/co-stack.com/typo3-extensions/logs/-/tree/5.3.1" - }, - "time": "2025-11-10T17:16:39+00:00" - }, { "name": "composer/ca-bundle", "version": "1.5.12", @@ -425,16 +364,16 @@ }, { "name": "composer/composer", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc" + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/c13824d95608b15913a7c0def0a3dea4474b71fc", - "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc", + "url": "https://api.github.com/repos/composer/composer/zipball/4120703b9bda8795075047b40361d7ec4d2abe49", + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49", "shasum": "" }, "require": { @@ -522,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.0" + "source": "https://github.com/composer/composer/tree/2.10.1" }, "funding": [ { @@ -534,7 +473,7 @@ "type": "github" } ], - "time": "2026-05-28T09:22:08+00:00" + "time": "2026-06-04T08:25:59+00:00" }, { "name": "composer/metadata-minifier", @@ -953,83 +892,6 @@ }, "time": "2025-09-16T12:23:56+00:00" }, - { - "name": "doctrine/annotations", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", - "shasum": "" - }, - "require": { - "doctrine/lexer": "^2 || ^3", - "ext-tokenizer": "*", - "php": "^7.2 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^2.0", - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.10.28", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6.4 || ^7", - "vimeo/psalm": "^4.30 || ^5.14" - }, - "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.2" - }, - "abandoned": true, - "time": "2024-09-05T10:17:24+00:00" - }, { "name": "doctrine/dbal", "version": "4.4.3", @@ -1184,97 +1046,6 @@ }, "time": "2026-02-07T07:09:04+00:00" }, - { - "name": "doctrine/event-manager", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", - "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "conflict": { - "doctrine/common": "<2.9" - }, - "require-dev": { - "doctrine/coding-standard": "^14", - "phpdocumentor/guides-cli": "^1.4", - "phpstan/phpstan": "^2.1.32", - "phpunit/phpunit": "^10.5.58" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", - "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" - ], - "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.1.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], - "time": "2026-01-29T07:11:08+00:00" - }, { "name": "doctrine/instantiator", "version": "2.1.0", @@ -1539,10 +1310,10 @@ "dist": { "type": "path", "url": "./packages/vitec", - "reference": "4c53c0e0e5a9a10369459cc9deaa39ebc0c8fd04" + "reference": "84868e73ce3f504a78e7140ecae3588862012053" }, "require": { - "typo3/cms-core": "^13.4" + "typo3/cms-core": "^13.4 || ^14.0" }, "replace": { "typo3-ter/vitec": "self.version" @@ -1638,31 +1409,31 @@ }, { "name": "friendsoftypo3/content-blocks", - "version": "1.6.0", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/FriendsOfTYPO3/content-blocks.git", - "reference": "a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca" + "reference": "6e82652bc653b98aabd4fc60211286c40c8abe75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca", - "reference": "a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca", + "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/6e82652bc653b98aabd4fc60211286c40c8abe75", + "reference": "6e82652bc653b98aabd4fc60211286c40c8abe75", "shasum": "" }, "require": { "opis/json-schema": "^2.6", "php": "^8.2", - "symfony/console": "^7.2", - "symfony/dependency-injection": "^7.2", - "symfony/filesystem": "^7.2", - "symfony/finder": "^7.2", - "symfony/var-exporter": "^7.2 || ^8.0", - "symfony/yaml": "^7.2", - "typo3/cms-backend": "^13.4.19", - "typo3/cms-core": "^13.4.19", - "typo3/cms-fluid": "^13.4.19", - "typo3/cms-frontend": "^13.4.19" + "symfony/console": "^7.4", + "symfony/dependency-injection": "^7.4", + "symfony/filesystem": "^7.4", + "symfony/finder": "^7.4", + "symfony/var-exporter": "^7.4 || ^8.0", + "symfony/yaml": "^7.4", + "typo3/cms-backend": "^14.3", + "typo3/cms-core": "^14.3", + "typo3/cms-fluid": "^14.3", + "typo3/cms-frontend": "^14.3" }, "replace": { "contentblocks/content-blocks": "*" @@ -1671,26 +1442,30 @@ "bnf/phpstan-psr-container": "^1.1", "contentblocks/examples": "@dev", "friendsofphp/php-cs-fixer": "^3.22", - "phpstan/phpdoc-parser": "^1.33", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "typo3/cms-extbase": "^13.4.19", - "typo3/cms-extensionmanager": "^13.4.19", - "typo3/cms-filelist": "^13.4.19", - "typo3/cms-install": "^13.4.19", - "typo3/cms-lowlevel": "^13.4.19", - "typo3/cms-rte-ckeditor": "^13.4.19", - "typo3/cms-tstemplate": "^13.4.19", - "typo3/cms-workspaces": "^13.4.19", + "typo3/cms-extbase": "^14.3", + "typo3/cms-extensionmanager": "^14.3", + "typo3/cms-filelist": "^14.3", + "typo3/cms-install": "^14.3", + "typo3/cms-lowlevel": "^14.3", + "typo3/cms-rte-ckeditor": "^14.3", + "typo3/cms-tstemplate": "^14.3", + "typo3/cms-workspaces": "^14.3", "typo3/coding-standards": "0.8.x-dev", - "typo3/testing-framework": "^9" + "typo3/testing-framework": "^9.5" }, "type": "typo3-cms-extension", "extra": { "typo3/cms": { "Package": { - "serviceProvider": "TYPO3\\CMS\\ContentBlocks\\ServiceProvider" + "serviceProvider": "TYPO3\\CMS\\ContentBlocks\\ServiceProvider", + "providesPackages": { + "opis/json-schema": "", + "symfony/var-exporter": "" + } }, + "version": "2.4.5", "web-dir": ".Build/public", "extension-key": "content_blocks" } @@ -1715,50 +1490,48 @@ "homepage": "https://typo3.org", "support": { "chat": "https://typo3.slack.com/archives/C8Z2UM50Q", - "docs": "https://docs.typo3.org/p/friendsoftypo3/content-blocks/1.4/en-us/", + "docs": "https://docs.typo3.org/p/friendsoftypo3/content-blocks/main/en-us/", "issues": "https://github.com/FriendsOfTYPO3/content-blocks/issues", "source": "https://github.com/FriendsOfTYPO3/content-blocks" }, - "time": "2026-05-26T09:43:56+00:00" + "time": "2026-06-04T16:27:24+00:00" }, { "name": "friendsoftypo3/headless", - "version": "v4.8.0", + "version": "v5.0.0-rc1", "source": { "type": "git", "url": "https://github.com/TYPO3-Headless/headless.git", - "reference": "971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9" + "reference": "53eb4233a2b94fe47b88fa00a3f9859ba4eb1419" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-Headless/headless/zipball/971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9", - "reference": "971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9", + "url": "https://api.github.com/repos/TYPO3-Headless/headless/zipball/53eb4233a2b94fe47b88fa00a3f9859ba4eb1419", + "reference": "53eb4233a2b94fe47b88fa00a3f9859ba4eb1419", "shasum": "" }, "require": { "ext-json": "*", "php": ">= 8.2", - "typo3/cms-core": "^12.4 || ^13.4", - "typo3/cms-install": "^12.4 || ^13.4" + "typo3/cms-core": "^14.0", + "typo3/cms-install": "^14.0" }, "conflict": { - "typo3/cms-core": "<12.4.21 || >=13.4 <13.4.18" + "typo3/cms-core": "<14.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.43", "friendsofphp/php-cs-fixer": "^v3", "helmich/typo3-typoscript-lint": "^v3", "justinrainbow/json-schema": "^5", - "phpspec/prophecy-phpunit": "^2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.12", "phpunit/phpcov": "^8 || ^9 || ^10", - "saschaegerer/phpstan-typo3": "^1.1", "seld/jsonlint": "^1.11", "symfony/yaml": "^6.1 || ^7.1", - "typo3/cms-form": "^12.4 || ^13.3", + "typo3/cms-form": "^14.0", "typo3/coding-standards": "^0.8", - "typo3/testing-framework": "^8.0" + "typo3/testing-framework": "^9.0" }, "type": "typo3-cms-extension", "extra": { @@ -1801,7 +1574,7 @@ ], "support": { "issues": "https://github.com/TYPO3-Headless/headless/issues", - "source": "https://github.com/TYPO3-Headless/headless/tree/v4.8.0" + "source": "https://github.com/TYPO3-Headless/headless/tree/v5.0.0-rc1" }, "funding": [ { @@ -1809,29 +1582,30 @@ "type": "custom" } ], - "time": "2026-05-08T13:50:24+00:00" + "time": "2026-05-08T15:09:37+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.10.5", + "version": "7.11.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" + "reference": "c987f8ce84b8434fa430795eca0f3430663da72b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", + "reference": "c987f8ce84b8434fa430795eca0f3430663da72b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.11", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1920,7 +1694,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.5" + "source": "https://github.com/guzzle/guzzle/tree/7.11.0" }, "funding": [ { @@ -1936,24 +1710,25 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:53:46+00:00" + "time": "2026-06-02T12:40:51+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -2003,7 +1778,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.4.1" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -2019,27 +1794,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:57:30+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.10.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130" + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7c1472269227dc6f18930bd903d7a88fe6c52130", - "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -2120,7 +1897,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.10.3" + "source": "https://github.com/guzzle/psr7/tree/2.11.0" }, "funding": [ { @@ -2136,75 +1913,7 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:48:20+00:00" - }, - { - "name": "itplusx/headless-container", - "version": "v3.0.0", - "source": { - "type": "git", - "url": "https://github.com/itplusx/headless-container.git", - "reference": "70b6a44e156b248c8306f7a4d8d14883f7bb18d7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/itplusx/headless-container/zipball/70b6a44e156b248c8306f7a4d8d14883f7bb18d7", - "reference": "70b6a44e156b248c8306f7a4d8d14883f7bb18d7", - "shasum": "" - }, - "require": { - "b13/container": ">=3.1.2", - "friendsoftypo3/headless": "^4.0", - "php": "^8.1", - "typo3/cms-core": "^12.4 || ^13.4" - }, - "replace": { - "typo3-ter/headless-container": "self.version" - }, - "type": "typo3-cms-extension", - "extra": { - "typo3/cms": { - "app-dir": ".Build", - "web-dir": ".Build/public", - "extension-key": "headless_container" - }, - "branch-alias": { - "dev-main": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ITplusX\\HeadlessContainer\\": "Classes/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ramón Schlosser", - "email": "schlosser@itplusx.de", - "homepage": "https://itplusx.de", - "role": "Developer" - } - ], - "description": "Container Content Elements (EXT:container) json output for EXT:headless", - "homepage": "https://itplusx.de", - "keywords": [ - "Container Content Elements", - "TYPO3 CMS", - "container", - "extension", - "headless", - "typo3" - ], - "support": { - "email": "typo3@itplusx.de", - "issues": "https://github.com/itplusx/headless-container/issues", - "source": "https://github.com/itplusx/headless-container/tree/v3.0.0" - }, - "time": "2025-07-01T13:46:20+00:00" + "time": "2026-06-02T12:30:48+00:00" }, { "name": "justinrainbow/json-schema", @@ -2488,22 +2197,22 @@ }, { "name": "netzbewegung/nb-headless-content-blocks", - "version": "0.0.21", + "version": "0.0.23", "source": { "type": "git", "url": "https://github.com/Netzbewegung-Backend/nb_headless_content_blocks.git", - "reference": "b4b8fb8f7fdafdcbbb8f199843ddd412d995305f" + "reference": "8b49a1b5e4c552445159d303a7acf0b69087e8e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Netzbewegung-Backend/nb_headless_content_blocks/zipball/b4b8fb8f7fdafdcbbb8f199843ddd412d995305f", - "reference": "b4b8fb8f7fdafdcbbb8f199843ddd412d995305f", + "url": "https://api.github.com/repos/Netzbewegung-Backend/nb_headless_content_blocks/zipball/8b49a1b5e4c552445159d303a7acf0b69087e8e7", + "reference": "8b49a1b5e4c552445159d303a7acf0b69087e8e7", "shasum": "" }, "require": { - "friendsoftypo3/content-blocks": ">=1.2.3", - "friendsoftypo3/headless": "^4.5", - "typo3/cms-core": "^13.4" + "friendsoftypo3/content-blocks": ">=1.2.3 || ^2.0", + "friendsoftypo3/headless": "^4.5 || ^5.0", + "typo3/cms-core": "^13.4 || ^14.3" }, "type": "typo3-cms-extension", "extra": { @@ -2551,9 +2260,9 @@ ], "support": { "issues": "https://github.com/Netzbewegung-Backend/nb_headless_content_blocks/issues", - "source": "https://github.com/Netzbewegung-Backend/nb_headless_content_blocks/tree/0.0.21" + "source": "https://github.com/Netzbewegung-Backend/nb_headless_content_blocks/tree/0.0.23" }, - "time": "2025-11-01T23:22:36+00:00" + "time": "2026-05-09T13:42:39+00:00" }, { "name": "nikic/php-parser", @@ -2613,70 +2322,6 @@ }, "time": "2025-12-06T11:56:16+00:00" }, - { - "name": "nitsan/ns-license", - "version": "13.0.14", - "source": { - "type": "git", - "url": "https://github.com/nitsan-technologies/ns_license.git", - "reference": "02a790d1e42ee7e30c35ffcabc4d51c52737232c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nitsan-technologies/ns_license/zipball/02a790d1e42ee7e30c35ffcabc4d51c52737232c", - "reference": "02a790d1e42ee7e30c35ffcabc4d51c52737232c", - "shasum": "" - }, - "require": { - "typo3/cms-core": "^12 || ^13", - "typo3/cms-extensionmanager": "^12 || ^13" - }, - "replace": { - "typo3-ter/ns-license": "self.version" - }, - "type": "typo3-cms-extension", - "extra": { - "typo3/cms": { - "web-dir": ".Build/public", - "extension-key": "ns_license", - "cms-package-dir": "{$vendor-dir}/typo3/cms" - } - }, - "autoload": { - "psr-4": { - "NITSAN\\NsLicense\\": "Classes/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Team T3Planet", - "email": "support@t3planet.com", - "homepage": "https://t3planet.com", - "role": "Developer" - } - ], - "description": "Manage License(s) of your purchased from T3Planet such as TYPO3 Templates and TYPO3 Extensions.", - "homepage": "https://t3planet.com", - "keywords": [ - "TYPO3 CMS", - "bootstrap", - "frontend", - "license", - "ns", - "responsive", - "template", - "theme" - ], - "support": { - "issues": "https://github.com/nitsan-technologies/ns_license/issues", - "source": "https://github.com/nitsan-technologies/ns_license/tree/13.0.14" - }, - "time": "2025-12-15T13:44:07+00:00" - }, { "name": "opis/json-schema", "version": "2.6.0", @@ -2922,16 +2567,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.7", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2939,8 +2584,8 @@ "ext-filter": "*", "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { @@ -2950,7 +2595,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -2980,44 +2626,44 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-18T20:47:46+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.12.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -3038,9 +2684,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2025-11-21T15:09:14+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -6838,6 +6484,188 @@ ], "time": "2026-05-23T18:05:53+00:00" }, + { + "name": "symfony/translation", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T11:19:24+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, { "name": "symfony/type-info", "version": "v8.0.9", @@ -6998,6 +6826,110 @@ ], "time": "2026-04-30T15:19:22+00:00" }, + { + "name": "symfony/validator", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/c76458623af9a3fe3b2e5b09b36453f334c2a361", + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/type-info": "^7.1.8", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-05T15:30:56+00:00" + }, { "name": "symfony/var-exporter", "version": "v8.0.9", @@ -7156,35 +7088,36 @@ }, { "name": "typo3/class-alias-loader", - "version": "v1.2.2", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/TYPO3/class-alias-loader.git", - "reference": "9e385e64ddf8a1ad354a4d62d4d0f90bce4dcbc2" + "reference": "c44de30ec91e134e28d4e886bc642bacaf4f407f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/class-alias-loader/zipball/9e385e64ddf8a1ad354a4d62d4d0f90bce4dcbc2", - "reference": "9e385e64ddf8a1ad354a4d62d4d0f90bce4dcbc2", + "url": "https://api.github.com/repos/TYPO3/class-alias-loader/zipball/c44de30ec91e134e28d4e886bc642bacaf4f407f", + "reference": "c44de30ec91e134e28d4e886bc642bacaf4f407f", "shasum": "" }, "require": { "composer-plugin-api": "^2.0", - "php": ">=7.1" + "php": ">=8.2" }, "replace": { "helhum/class-alias-loader": "*" }, "require-dev": { "composer/composer": "^2.0@dev", + "friendsofphp/php-cs-fixer": "^3.95", "mikey179/vfsstream": "~1.4.0@dev", - "phpunit/phpunit": "^8 || ^9" + "phpunit/phpunit": "^11" }, "type": "composer-plugin", "extra": { "class": "TYPO3\\ClassAliasLoader\\Plugin", "branch-alias": { - "dev-main": "1.1.x-dev" + "dev-main": "2.0.x-dev" } }, "autoload": { @@ -7212,29 +7145,29 @@ ], "support": { "issues": "https://github.com/TYPO3/class-alias-loader/issues", - "source": "https://github.com/TYPO3/class-alias-loader/tree/v1.2.2" + "source": "https://github.com/TYPO3/class-alias-loader/tree/v2.0.1" }, - "time": "2026-04-17T09:41:06+00:00" + "time": "2026-04-17T13:11:31+00:00" }, { "name": "typo3/cms-backend", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/backend.git", - "reference": "9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1" + "reference": "e3a61d3f083e10e8125f281b5c758103cfd015c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1", - "reference": "9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1", + "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/e3a61d3f083e10e8125f281b5c758103cfd015c4", + "reference": "e3a61d3f083e10e8125f281b5c758103cfd015c4", "shasum": "" }, "require": { "ext-intl": "*", "ext-libxml": "*", "psr/event-dispatcher": "^1.0", - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7245,12 +7178,13 @@ "typo3/cms-cshmanual": "self.version", "typo3/cms-func-wizards": "self.version", "typo3/cms-recordlist": "self.version", + "typo3/cms-setup": "self.version", "typo3/cms-t3editor": "self.version", "typo3/cms-wizard-crpages": "self.version", "typo3/cms-wizard-sortpages": "self.version" }, "suggest": { - "typo3/cms-install": "To generate url to install tool in environment toolbar" + "typo3/cms-install": "Displays a link to the Environment module in the System Information toolbar." }, "type": "typo3-cms-framework", "extra": { @@ -7264,7 +7198,7 @@ "extension-key": "backend" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" }, "typo3/class-alias-loader": { "class-alias-maps": [ @@ -7274,7 +7208,8 @@ }, "autoload": { "psr-4": { - "TYPO3\\CMS\\Backend\\": "Classes/" + "TYPO3\\CMS\\Backend\\": "Classes/", + "TYPO3\\CMS\\Setup\\Event\\": "DeprecatedClasses/Setup/Event/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7289,31 +7224,40 @@ } ], "description": "TYPO3 CMS backend", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-belog", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/belog.git", - "reference": "e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55" + "reference": "5c0585ae27c0712b38f36225e1c564f47b7d293d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55", - "reference": "e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55", + "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/5c0585ae27c0712b38f36225e1c564f47b7d293d", + "reference": "5c0585ae27c0712b38f36225e1c564f47b7d293d", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7327,7 +7271,7 @@ "extension-key": "belog" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7347,31 +7291,40 @@ } ], "description": "TYPO3 CMS Log - View logs from the sys_log table in the TYPO3 backend modules System>Log", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-beuser", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/beuser.git", - "reference": "554788f60609f4b1ed61f46e584fb4277e931629" + "reference": "7269663f07577c1a17674207c8aff5858adf9a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/554788f60609f4b1ed61f46e584fb4277e931629", - "reference": "554788f60609f4b1ed61f46e584fb4277e931629", + "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/7269663f07577c1a17674207c8aff5858adf9a20", + "reference": "7269663f07577c1a17674207c8aff5858adf9a20", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7385,7 +7338,7 @@ "extension-key": "beuser" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7404,15 +7357,24 @@ "role": "Developer" } ], - "description": "TYPO3 CMS Backend User - TYPO3 backend module System>Backend Users for managing backend users and groups.", - "homepage": "https://typo3.org", + "description": "TYPO3 CMS Backend User - TYPO3 backend module Administration > Users for managing backend users and groups.", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-cli", @@ -7521,25 +7483,24 @@ }, { "name": "typo3/cms-core", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/core.git", - "reference": "7779fe79725633fdafb18acb35b40c047acc1c0f" + "reference": "e323e061321a6a8441d2b552dfe59af4de478d2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/7779fe79725633fdafb18acb35b40c047acc1c0f", - "reference": "7779fe79725633fdafb18acb35b40c047acc1c0f", + "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/e323e061321a6a8441d2b552dfe59af4de478d2c", + "reference": "e323e061321a6a8441d2b552dfe59af4de478d2c", "shasum": "" }, "require": { - "bacon/bacon-qr-code": "^3.0", + "bacon/bacon-qr-code": "^3.1.1", "christian-riesen/base32": "^1.6", "composer-runtime-api": "^2.1", - "doctrine/annotations": "^2.0.2", + "composer/semver": "^3.4", "doctrine/dbal": "~4.4.3", - "doctrine/event-manager": "^2.0.1", "doctrine/lexer": "^3.0.1", "egulias/email-validator": "^4.0", "enshrined/svg-sanitize": "~0.22", @@ -7551,12 +7512,13 @@ "ext-pcre": "*", "ext-pdo": "*", "ext-session": "*", + "ext-sodium": "*", "ext-tokenizer": "*", "ext-xml": "*", - "firebase/php-jwt": "^6.10.2 || ^7.0.2", + "firebase/php-jwt": "^6.10.2 || ^7.0.5", "guzzlehttp/guzzle": "^7.9.2", - "guzzlehttp/psr7": "^2.7.0", - "lolli42/finediff": "^1.1.1", + "guzzlehttp/psr7": "^2.9.0", + "lolli42/finediff": "^1.1.2", "masterminds/html5": "^2.10.0", "php": "^8.2", "psr/container": "^2.0", @@ -7567,30 +7529,31 @@ "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", "psr/log": "^3.0.1", - "symfony/config": "^7.2", - "symfony/console": "^7.2", - "symfony/dependency-injection": "^7.2", - "symfony/doctrine-messenger": "^7.2", - "symfony/event-dispatcher-contracts": "^3.1", - "symfony/expression-language": "^7.2", - "symfony/filesystem": "^7.2", - "symfony/finder": "^7.2", - "symfony/http-foundation": "^7.2", + "symfony/config": "^7.4.8", + "symfony/console": "^7.4.8", + "symfony/dependency-injection": "^7.4.8", + "symfony/doctrine-messenger": "^7.4.6", + "symfony/event-dispatcher-contracts": "^3.6.0", + "symfony/expression-language": "^7.4.8", + "symfony/filesystem": "^7.4.8", + "symfony/finder": "^7.4.8", + "symfony/http-foundation": "^7.4.8", "symfony/mailer": "^7.4.12", - "symfony/messenger": "^7.2", + "symfony/messenger": "^7.4.8", "symfony/mime": "^7.4.12", - "symfony/options-resolver": "^7.2", - "symfony/polyfill-php83": "^1.31", - "symfony/process": "^7.2", - "symfony/rate-limiter": "^7.2", + "symfony/options-resolver": "^7.4.8", + "symfony/polyfill-php83": "^1.36.0", + "symfony/process": "^7.4.8", + "symfony/rate-limiter": "^7.4.8", "symfony/routing": "^7.4.12", - "symfony/uid": "^7.2", + "symfony/translation": "^7.4.8", + "symfony/uid": "^7.4.8", "symfony/yaml": "^7.4.12", - "typo3/class-alias-loader": "^1.2", - "typo3/cms-cli": "^3.1.1", + "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", - "typo3fluid/fluid": "^4.6.1" + "typo3fluid/fluid": "^5.3.1" }, "conflict": { "hoa/core": "*", @@ -7613,7 +7576,7 @@ "ext-mysqli": "", "ext-openssl": "OpenSSL is required for sending SMTP mails over an encrypted channel endpoint", "ext-zip": "", - "ext-zlib": "TYPO3 uses zlib for amongst others output compression and un/packing t3x extension files" + "ext-zlib": "TYPO3 uses zlib for resource compression and un/packing t3x extension files" }, "type": "typo3-cms-framework", "extra": { @@ -7627,7 +7590,7 @@ "extension-key": "core" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" }, "typo3/class-alias-loader": { "class-alias-maps": [ @@ -7641,7 +7604,10 @@ ], "psr-4": { "TYPO3\\CMS\\Core\\": "Classes/" - } + }, + "classmap": [ + "DeprecatedClasses/ext-install/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7655,35 +7621,44 @@ } ], "description": "TYPO3 CMS Core", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-dashboard", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/dashboard.git", - "reference": "80aca12c9743de9d1a53747fc16bef1b9351d643" + "reference": "195f573f0c44c239de385fb712969ea7455c7d7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/80aca12c9743de9d1a53747fc16bef1b9351d643", - "reference": "80aca12c9743de9d1a53747fc16bef1b9351d643", + "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/195f573f0c44c239de385fb712969ea7455c7d7b", + "reference": "195f573f0c44c239de385fb712969ea7455c7d7b", "shasum": "" }, "require": { - "typo3/cms-backend": "13.4.30", - "typo3/cms-core": "13.4.30", - "typo3/cms-extbase": "13.4.30", - "typo3/cms-fluid": "13.4.30", - "typo3/cms-frontend": "13.4.30" + "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" }, "conflict": { "typo3/cms": "*" @@ -7698,7 +7673,7 @@ "extension-key": "dashboard" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7718,37 +7693,48 @@ } ], "description": "TYPO3 CMS Dashboard - TYPO3 backend module used to configure and create backend widgets.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-extbase", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extbase.git", - "reference": "90f6fe6ecef724e42ade59decf6ff969d280a15e" + "reference": "c178b1246599c6619452b5ecbad980bc1a9cd9a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/90f6fe6ecef724e42ade59decf6ff969d280a15e", - "reference": "90f6fe6ecef724e42ade59decf6ff969d280a15e", + "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/c178b1246599c6619452b5ecbad980bc1a9cd9a1", + "reference": "c178b1246599c6619452b5ecbad980bc1a9cd9a1", "shasum": "" }, "require": { "doctrine/instantiator": "^1.5 || ^2.0", - "phpdocumentor/reflection-docblock": "^5.6.5", - "phpdocumentor/type-resolver": "^1.8.2", - "symfony/dependency-injection": "^7.2", - "symfony/property-access": "^7.2", - "symfony/property-info": "^7.2", - "typo3/cms-core": "13.4.30" + "phpdocumentor/reflection-docblock": "^6.0.3", + "phpdocumentor/type-resolver": "^2.0.0", + "phpstan/phpdoc-parser": "^2.1", + "symfony/dependency-injection": "^7.4.8", + "symfony/property-access": "^7.4.8", + "symfony/property-info": "^7.4.8", + "symfony/validator": "^7.4.8", + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7768,7 +7754,12 @@ "extension-key": "extbase" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" + }, + "typo3/class-alias-loader": { + "class-alias-maps": [ + "Migrations/Code/ClassAliasMap.php" + ] } }, "autoload": { @@ -7788,32 +7779,41 @@ } ], "description": "TYPO3 CMS Extbase - Extension framework to create TYPO3 frontend plugins and TYPO3 backend modules.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-extensionmanager", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extensionmanager.git", - "reference": "545ac7306bd6d8ee8fb520fc0107c78981474d90" + "reference": "613d92bb63eeaa6611fa8b426fa50282386d7614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/545ac7306bd6d8ee8fb520fc0107c78981474d90", - "reference": "545ac7306bd6d8ee8fb520fc0107c78981474d90", + "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/613d92bb63eeaa6611fa8b426fa50282386d7614", + "reference": "613d92bb63eeaa6611fa8b426fa50282386d7614", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7829,12 +7829,7 @@ "extension-key": "extensionmanager" }, "branch-alias": { - "dev-main": "13.4.x-dev" - }, - "typo3/class-alias-loader": { - "class-alias-maps": [ - "Migrations/Code/ClassAliasMap.php" - ] + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7853,32 +7848,41 @@ "role": "Developer" } ], - "description": "TYPO3 CMS Extension Manager - Backend module (Admin Tools>Extensions) for viewing and managing extensions.", - "homepage": "https://typo3.org", + "description": "TYPO3 CMS Extension Manager - Backend module (System > Extensions) for viewing and managing extensions.", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-felogin", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/felogin.git", - "reference": "0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d" + "reference": "c44e401f138491c494018390ccae58aa9c7d48b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d", - "reference": "0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d", + "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/c44e401f138491c494018390ccae58aa9c7d48b9", + "reference": "c44e401f138491c494018390ccae58aa9c7d48b9", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7892,7 +7896,7 @@ "extension-key": "felogin" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7912,31 +7916,40 @@ } ], "description": "TYPO3 CMS Frontend Login - A template-based plugin to log in website users in the TYPO3 frontend.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-felogin/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-felogin/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-filelist", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/filelist.git", - "reference": "9f16745597dd84831855d1ef04ed6c07769ae8fb" + "reference": "e4f20c9aa130a2d23e3de5bc904877ca07b071a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/9f16745597dd84831855d1ef04ed6c07769ae8fb", - "reference": "9f16745597dd84831855d1ef04ed6c07769ae8fb", + "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/e4f20c9aa130a2d23e3de5bc904877ca07b071a0", + "reference": "e4f20c9aa130a2d23e3de5bc904877ca07b071a0", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -7952,7 +7965,7 @@ "extension-key": "filelist" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -7971,35 +7984,45 @@ "role": "Developer" } ], - "description": "TYPO3 CMS Filelist - TYPO3 backend module (File>Filelist) used for managing files.", - "homepage": "https://typo3.org", + "description": "TYPO3 CMS Filelist - TYPO3 backend module 'Media' used for managing files.", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-fluid", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid.git", - "reference": "9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b" + "reference": "c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b", - "reference": "9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95", + "reference": "c2366cf6e7dfdf80d098f7b7f75ac0e7ac1e0b95", "shasum": "" }, "require": { - "symfony/dependency-injection": "^7.2", - "typo3/cms-core": "13.4.30", - "typo3/cms-extbase": "13.4.30", - "typo3fluid/fluid": "^4.6.1" + "symfony/dependency-injection": "^7.4.8", + "symfony/finder": "^7.4.8", + "typo3/cms-core": "14.3.2", + "typo3/cms-extbase": "14.3.2", + "typo3fluid/fluid": "^5.3.1" }, "conflict": { "typo3/cms": "*" @@ -8016,7 +8039,7 @@ "extension-key": "fluid" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8036,33 +8059,42 @@ } ], "description": "TYPO3 CMS Fluid Integration - Integration of the Fluid templating engine into TYPO3.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-fluid-styled-content", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid_styled_content.git", - "reference": "0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c" + "reference": "a3c108e57adec1fa71c8d29420eb0205bf090185" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c", - "reference": "0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/a3c108e57adec1fa71c8d29420eb0205bf090185", + "reference": "a3c108e57adec1fa71c8d29420eb0205bf090185", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30", - "typo3/cms-fluid": "13.4.30", - "typo3/cms-frontend": "13.4.30" + "typo3/cms-core": "14.3.2", + "typo3/cms-fluid": "14.3.2", + "typo3/cms-frontend": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8076,7 +8108,7 @@ "extension-key": "fluid_styled_content" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8096,34 +8128,43 @@ } ], "description": "TYPO3 CMS Fluid Styled Content - Fluid templates for TYPO3 content elements.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-fluid-styled-content/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-fluid-styled-content/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-form", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/form.git", - "reference": "fbe21be780125a642a265c6e25a76a7551de05d7" + "reference": "5d4d429f6870582b6debfee339fbf39891ed81f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/fbe21be780125a642a265c6e25a76a7551de05d7", - "reference": "fbe21be780125a642a265c6e25a76a7551de05d7", + "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/5d4d429f6870582b6debfee339fbf39891ed81f8", + "reference": "5d4d429f6870582b6debfee339fbf39891ed81f8", "shasum": "" }, "require": { "psr/http-message": "^1.1 || ^2.0", - "symfony/expression-language": "^7.2", - "typo3/cms-core": "13.4.30", - "typo3/cms-frontend": "13.4.30" + "symfony/expression-language": "^7.4.8", + "typo3/cms-core": "14.3.2", + "typo3/cms-frontend": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8142,7 +8183,7 @@ "extension-key": "form" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8162,32 +8203,41 @@ } ], "description": "TYPO3 CMS Form - Flexible TYPO3 frontend form framework that comes with a backend editor interface.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-form/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-form/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-frontend", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/frontend.git", - "reference": "d7588d8c2c426085b3b50338765b883dc8c77675" + "reference": "9a623a68e880bbf5ef468b4c1206648271dfe58a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/d7588d8c2c426085b3b50338765b883dc8c77675", - "reference": "d7588d8c2c426085b3b50338765b883dc8c77675", + "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/9a623a68e880bbf5ef468b4c1206648271dfe58a", + "reference": "9a623a68e880bbf5ef468b4c1206648271dfe58a", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8200,14 +8250,13 @@ "typo3/cms": { "Package": { "protected": true, - "serviceProvider": "TYPO3\\CMS\\Frontend\\ServiceProvider", "partOfFactoryDefault": true, "partOfMinimalUsableSystem": true }, "extension-key": "frontend" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" }, "typo3/class-alias-loader": { "class-alias-maps": [ @@ -8232,31 +8281,40 @@ } ], "description": "TYPO3 CMS Frontend", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-impexp", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/impexp.git", - "reference": "8a7fa24c720731783a0847a2f8250cfa07564898" + "reference": "9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/8a7fa24c720731783a0847a2f8250cfa07564898", - "reference": "8a7fa24c720731783a0847a2f8250cfa07564898", + "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e", + "reference": "9bbc42bc05358bf03dc1931a2ea9ecdfaf03c19e", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8270,7 +8328,7 @@ "extension-key": "impexp" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8290,31 +8348,40 @@ } ], "description": "TYPO3 CMS Import/Export - Tool for importing and exporting records using XML or the custom T3D format.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/c/typo3/cms-impexp/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-info", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/info.git", - "reference": "99a3a5b321b8b7db90de03ea1039217d1628a801" + "reference": "d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/99a3a5b321b8b7db90de03ea1039217d1628a801", - "reference": "99a3a5b321b8b7db90de03ea1039217d1628a801", + "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd", + "reference": "d89a05aa69b2ee7a1ee1c8a0292122e40faba5bd", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8331,7 +8398,7 @@ "extension-key": "info" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8351,38 +8418,47 @@ } ], "description": "TYPO3 CMS Info - TYPO3 backend module for displaying information, such as a pagetree overview and localization information.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-install", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/install.git", - "reference": "b5faf8708fa5322777104bbb9b31376882619cc3" + "reference": "033b815b778af0a6bd1a55fd8a8755dc7f42d9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/b5faf8708fa5322777104bbb9b31376882619cc3", - "reference": "b5faf8708fa5322777104bbb9b31376882619cc3", + "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/033b815b778af0a6bd1a55fd8a8755dc7f42d9e4", + "reference": "033b815b778af0a6bd1a55fd8a8755dc7f42d9e4", "shasum": "" }, "require": { "doctrine/dbal": "~4.4.3", "guzzlehttp/promises": "^2.0.3", "nikic/php-parser": "^5.4.0", - "symfony/finder": "^7.2", - "symfony/http-foundation": "^7.2", - "typo3/cms-core": "13.4.30", - "typo3/cms-extbase": "13.4.30", - "typo3/cms-fluid": "13.4.30" + "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" }, "conflict": { "typo3/cms": "*" @@ -8399,7 +8475,7 @@ "extension-key": "install" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8419,31 +8495,40 @@ } ], "description": "TYPO3 CMS Install Tool - The Install Tool is used for installation, upgrade, system administration and setup tasks.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-reactions", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/reactions.git", - "reference": "691910e92e9f57ad51a27acb587603ba2224c3ef" + "reference": "5e4215eabe684343da7ed2eeae472680cefc9c1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/691910e92e9f57ad51a27acb587603ba2224c3ef", - "reference": "691910e92e9f57ad51a27acb587603ba2224c3ef", + "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/5e4215eabe684343da7ed2eeae472680cefc9c1d", + "reference": "5e4215eabe684343da7ed2eeae472680cefc9c1d", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8457,7 +8542,7 @@ "extension-key": "reactions" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8477,31 +8562,40 @@ } ], "description": "TYPO3 CMS Reactions - Handle incoming Webhooks for TYPO3", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/c/typo3/cms-reactions/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-recycler", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/recycler.git", - "reference": "9768124ed8d1961de0170e60085a10f8d8b4f51e" + "reference": "d17e66c3bf8819b4915a582efe510636553ccfaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/9768124ed8d1961de0170e60085a10f8d8b4f51e", - "reference": "9768124ed8d1961de0170e60085a10f8d8b4f51e", + "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/d17e66c3bf8819b4915a582efe510636553ccfaa", + "reference": "d17e66c3bf8819b4915a582efe510636553ccfaa", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8518,7 +8612,7 @@ "extension-key": "recycler" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8538,38 +8632,44 @@ } ], "description": "TYPO3 CMS Recycler - Restore deleted records or remove them from the database permanently.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/c/typo3/cms-recycler/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-rte-ckeditor", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/rte_ckeditor.git", - "reference": "f0b63855ebebe990067c79d0d0465aacd88556aa" + "reference": "f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/f0b63855ebebe990067c79d0d0465aacd88556aa", - "reference": "f0b63855ebebe990067c79d0d0465aacd88556aa", + "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0", + "reference": "f1c80b0e15dc9b5cee51ffc3578fcbd3293dd1b0", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" }, - "suggest": { - "typo3/cms-setup": "*" - }, "type": "typo3-cms-framework", "extra": { "typo3/cms": { @@ -8579,7 +8679,7 @@ "extension-key": "rte_ckeditor" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8599,31 +8699,40 @@ } ], "description": "TYPO3 CMS RTE CKEditor - Integration of CKEditor as a Rich Text Editor for the TYPO3 backend.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-rte-ckeditor/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-rte-ckeditor/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-scheduler", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/scheduler.git", - "reference": "0a6688a0d52660250baae6c216c838c2d101cb66" + "reference": "398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/0a6688a0d52660250baae6c216c838c2d101cb66", - "reference": "0a6688a0d52660250baae6c216c838c2d101cb66", + "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850", + "reference": "398eb05a3590ee8d37fbfdfe8cb0967c0ca1e850", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8634,7 +8743,7 @@ "extension-key": "scheduler" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8654,33 +8763,42 @@ } ], "description": "TYPO3 CMS Scheduler - Schedule tasks to run once or periodically at a specific time.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-scheduler/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-scheduler/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-seo", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/seo.git", - "reference": "fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62" + "reference": "db45e6c092197c32596a9b4e59123784cf442b42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62", - "reference": "fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62", + "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/db45e6c092197c32596a9b4e59123784cf442b42", + "reference": "db45e6c092197c32596a9b4e59123784cf442b42", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30", - "typo3/cms-extbase": "13.4.30", - "typo3/cms-frontend": "13.4.30" + "typo3/cms-core": "14.3.2", + "typo3/cms-extbase": "14.3.2", + "typo3/cms-frontend": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8697,7 +8815,7 @@ "extension-key": "seo" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8717,89 +8835,40 @@ } ], "description": "TYPO3 CMS SEO - SEO features including specific fields for SEO purposes, rendering of HTML meta tags and sitemaps.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org/c/typo3/cms-seo/main/en-us", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/c/typo3/cms-seo/main/en-us/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" - }, - { - "name": "typo3/cms-setup", - "version": "v13.4.30", - "source": { - "type": "git", - "url": "https://github.com/TYPO3-CMS/setup.git", - "reference": "ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/setup/zipball/ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f", - "reference": "ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f", - "shasum": "" - }, - "require": { - "typo3/cms-core": "13.4.30" - }, - "conflict": { - "typo3/cms": "*" - }, - "type": "typo3-cms-framework", - "extra": { - "typo3/cms": { - "Package": { - "partOfFactoryDefault": true - }, - "extension-key": "setup" - }, - "branch-alias": { - "dev-main": "13.4.x-dev" - } - }, - "autoload": { - "psr-4": { - "TYPO3\\CMS\\Setup\\": "Classes/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ + "funding": [ { - "name": "TYPO3 Core Team", - "email": "typo3cms@typo3.org", - "role": "Developer" + "url": "https://typo3.org/membership", + "type": "membership" } ], - "description": "TYPO3 CMS Setup - Allows users to edit a limited set of options for their user profile, including preferred language, their name and email address.", - "homepage": "https://typo3.org", - "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" - }, - "time": "2026-05-26T09:34:34+00:00" + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-sys-note", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/sys_note.git", - "reference": "b1d87efc54ffb9df6a9c5d24cd83d9833a730d32" + "reference": "e16ca23b306d844997dc97998cbb7c77d7624f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/b1d87efc54ffb9df6a9c5d24cd83d9833a730d32", - "reference": "b1d87efc54ffb9df6a9c5d24cd83d9833a730d32", + "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/e16ca23b306d844997dc97998cbb7c77d7624f9a", + "reference": "e16ca23b306d844997dc97998cbb7c77d7624f9a", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8816,7 +8885,7 @@ "extension-key": "sys_note" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8836,31 +8905,40 @@ } ], "description": "TYPO3 CMS System Notes - Records with messages which can be placed on any page and contain instructions or other information related to a page or section.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-tstemplate", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/tstemplate.git", - "reference": "76f71c2a8468df1ec0c78e1cf936af36c2d64123" + "reference": "e3f09b0da26a57e25a07223253743f009b6c51ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/76f71c2a8468df1ec0c78e1cf936af36c2d64123", - "reference": "76f71c2a8468df1ec0c78e1cf936af36c2d64123", + "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/e3f09b0da26a57e25a07223253743f009b6c51ab", + "reference": "e3f09b0da26a57e25a07223253743f009b6c51ab", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8874,7 +8952,7 @@ "extension-key": "tstemplate" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8894,31 +8972,40 @@ } ], "description": "TYPO3 CMS TypoScript - TYPO3 backend module for the management of TypoScript records for the CMS frontend.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-viewpage", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/viewpage.git", - "reference": "826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0" + "reference": "b766123bdb09d627b5ad4bef09bf2aa401243927" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0", - "reference": "826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0", + "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/b766123bdb09d627b5ad4bef09bf2aa401243927", + "reference": "b766123bdb09d627b5ad4bef09bf2aa401243927", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.30" + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8932,7 +9019,7 @@ "extension-key": "viewpage" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -8952,32 +9039,41 @@ } ], "description": "TYPO3 CMS Viewpage - Use the (Web>View) backend module to view a frontend page inside the TYPO3 backend.", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", - "docs": "https://docs.typo3.org", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "chat": "https://typo3.community/meet/slack/", + "docs": "https://docs.typo3.org/", + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/cms-webhooks", - "version": "v13.4.30", + "version": "v14.3.2", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/webhooks.git", - "reference": "381a808de044da905cf20245ec652536dc5c2d27" + "reference": "2abedd921aac6a228c94eec6d0d68f5bd3769dc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/381a808de044da905cf20245ec652536dc5c2d27", - "reference": "381a808de044da905cf20245ec652536dc5c2d27", + "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/2abedd921aac6a228c94eec6d0d68f5bd3769dc0", + "reference": "2abedd921aac6a228c94eec6d0d68f5bd3769dc0", "shasum": "" }, "require": { - "symfony/uid": "^7.2", - "typo3/cms-core": "13.4.30" + "symfony/uid": "^7.4.8", + "typo3/cms-core": "14.3.2" }, "conflict": { "typo3/cms": "*" @@ -8991,7 +9087,7 @@ "extension-key": "webhooks" }, "branch-alias": { - "dev-main": "13.4.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -9011,14 +9107,23 @@ } ], "description": "TYPO3 CMS Webhooks - Handle outgoing Webhooks for TYPO3", - "homepage": "https://typo3.org", + "homepage": "https://typo3.community/", "support": { - "chat": "https://typo3.org/help", + "chat": "https://typo3.community/meet/slack/", "docs": "https://docs.typo3.org/c/typo3/cms-webhooks/main/en-us/", - "issues": "https://forge.typo3.org", - "source": "https://github.com/typo3/typo3" + "forum": "https://talk.typo3.org/", + "issues": "https://forge.typo3.org/issues/", + "rss": "https://news.typo3.com/rss/", + "security": "https://typo3.org/security/", + "source": "https://github.com/TYPO3/typo3/" }, - "time": "2026-05-26T09:34:34+00:00" + "funding": [ + { + "url": "https://typo3.org/membership", + "type": "membership" + } + ], + "time": "2026-05-26T09:37:47+00:00" }, { "name": "typo3/html-sanitizer", @@ -9073,16 +9178,16 @@ }, { "name": "typo3fluid/fluid", - "version": "4.6.1", + "version": "5.3.1", "source": { "type": "git", "url": "https://github.com/TYPO3/Fluid.git", - "reference": "7dc645eba0cb1ea4d2418252e00db4a4733e9b06" + "reference": "28c42c75b171aef196ddbe151b0d1146c290249d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/Fluid/zipball/7dc645eba0cb1ea4d2418252e00db4a4733e9b06", - "reference": "7dc645eba0cb1ea4d2418252e00db4a4733e9b06", + "url": "https://api.github.com/repos/TYPO3/Fluid/zipball/28c42c75b171aef196ddbe151b0d1146c290249d", + "reference": "28c42c75b171aef196ddbe151b0d1146c290249d", "shasum": "" }, "require": { @@ -9093,11 +9198,12 @@ "ext-json": "*", "ext-simplexml": "*", "friendsofphp/php-cs-fixer": "^3.94.2", + "infection/infection": "^0.32.6", "phpstan/phpstan": "^2.1.40", "phpstan/phpstan-phpunit": "^2.0.16", "phpunit/phpunit": "^11.5.55 || ^12.5.14 || ^13.0.5", "psr/container": "^2.0.2", - "t3docs/fluid-documentation-generator": "^4.4.1" + "t3docs/fluid-documentation-generator": "^4.4.2" }, "suggest": { "ext-json": "PHP JSON is needed when using JSONVariableProvider: A relatively rare use case", @@ -9109,7 +9215,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.x-dev" + "dev-main": "5.x-dev" } }, "autoload": { @@ -9128,55 +9234,7 @@ "issues": "https://github.com/TYPO3/Fluid/issues", "source": "https://github.com/TYPO3/Fluid" }, - "time": "2026-04-16T17:21:26+00:00" - }, - { - "name": "w3code/w3c-categorymanager", - "version": "v13.4.2", - "source": { - "type": "git", - "url": "https://github.com/w3code-tn/w3c_categorymanager.git", - "reference": "2e1a92636f8c9f35bbe716989efef4ab590426b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/w3code-tn/w3c_categorymanager/zipball/2e1a92636f8c9f35bbe716989efef4ab590426b1", - "reference": "2e1a92636f8c9f35bbe716989efef4ab590426b1", - "shasum": "" - }, - "require": { - "typo3/cms-core": "^12.4 || ^13.4" - }, - "replace": { - "typo3-ter/w3c_categorymanager": "self.version" - }, - "type": "typo3-cms-extension", - "extra": { - "typo3/cms": { - "extension-key": "w3c_categorymanager" - } - }, - "autoload": { - "psr-4": { - "W3code\\W3cCategorymanager\\": "Classes/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Mehdi Guermazi", - "email": "mehdi.guermazi@w3code.tn" - } - ], - "description": "Manage system categories easily with a category tree", - "support": { - "issues": "https://github.com/w3code-tn/w3c_categorymanager/issues", - "source": "https://github.com/w3code-tn/w3c_categorymanager/tree/v13.4.2" - }, - "time": "2025-08-29T16:53:08+00:00" + "time": "2026-04-16T17:09:54+00:00" }, { "name": "webmozart/assert", @@ -9248,7 +9306,9 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "friendsoftypo3/headless": 5 + }, "prefer-stable": false, "prefer-lowest": false, "platform": {}, diff --git a/config/sites/vitec/config.yaml b/config/sites/vitec/config.yaml index 0f8c708..98a6d17 100644 --- a/config/sites/vitec/config.yaml +++ b/config/sites/vitec/config.yaml @@ -7,12 +7,12 @@ dependencies: - typo3/seo-sitemap - friendsoftypo3/headless - friendsoftypo3/headless-mixed - - itplusx/headless-container - nb-headless-content-blocks/headless-content-blocks - evomedien/vitecset - vitec/content-blocks-bundle - vitec/cta-banner - vitec/herosection + - vitec/card frontendBase: '' headless: 1 languages: diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 b/packages/vitec/Classes/DataProcessing/.msys00000000000000004cda359073ead130 old mode 100755 new mode 100644 similarity index 62% rename from packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 rename to packages/vitec/Classes/DataProcessing/.msys00000000000000004cda359073ead130 index 7bc36d7..7e65ce9 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 +++ b/packages/vitec/Classes/DataProcessing/.msys00000000000000004cda359073ead130 @@ -3,6 +3,13 @@ declare(strict_types=1); namespace Evomedien\Vitec\DataProcessing; +use Evomedien\Vitec\UserFunc\ProductListJsonRenderer; +use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer; +use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer; +use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer; +use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer; +use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; +use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -15,6 +22,11 @@ use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; * { id, type, colPos, sorting, appearance, data } * `data` only contains non-empty content-relevant fields. * + * VITEC list-plugins nested as container children (productlist, productshow, + * usecaselist, usecaseshow) are resolved to the SAME JSON the top-level + * headless rendering produces, injected into `data` under their respective + * key. The raw pi_flexform XML is then dropped from `data`. + * * Exception-safe. */ final class ContainerChildrenProcessor implements DataProcessorInterface @@ -27,7 +39,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface /** Technical / system / TCA-default fields — never sent to frontend. */ private const SYSTEM_FIELDS = [ - // versioning / language / workspace / housekeeping 'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource', 'l10n_source', 'l10n_state', 'l10n_parent', 't3_origuid', 'tx_impexp_origuid', @@ -38,8 +49,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp', 'editlock', 'sorting_foreign', 'rowDescription', 'spaceBefore', 'spaceAfter', // legacy - // TCA defaults that TYPO3 always sets on every tt_content row, - // regardless of CType — rarely relevant to the frontend: 'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date', 'bullets_type', 'cols', 'table_delimiter', 'table_enclosure', 'table_header_position', @@ -53,6 +62,22 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'header_layout', ]; + /** + * Nested VITEC list-plugins: list_type => [ rendererClass, jsonKey ]. + * The renderer's renderForRecord() is invoked with the child's own + * tt_content row so the result is record-accurate (works with multiple + * containers / plugins on the same page). + */ + private const PLUGIN_RENDERERS = [ + 'vitec_productlist' => [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + ]; + public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, @@ -124,6 +149,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface $data[$field] = $this->castValue($field, $value); } + // Resolve nested VITEC list-plugins to their headless JSON. + $this->resolvePluginData($record, $data); + return [ 'id' => (int)$record['uid'], 'type' => (string)$record['CType'], @@ -139,6 +167,52 @@ final class ContainerChildrenProcessor implements DataProcessorInterface ]; } + /** + * If the child is a VITEC list-plugin, run its JSON renderer for THIS + * record and inject the decoded result under its key. Drops the raw + * pi_flexform XML afterwards. Never throws. + * + * @param array $record + * @param array $data + */ + private function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + + // The raw FlexForm XML is noise once the plugin is resolved. + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // Leave the raw data untouched on any failure. + } + } + private function isEmpty(mixed $value): bool { return $value === null diff --git a/packages/vitec/Classes/DataProcessing/.msys00000000000000004dd18ef4cfaf2af9 b/packages/vitec/Classes/DataProcessing/.msys00000000000000004dd18ef4cfaf2af9 new file mode 100644 index 0000000..c3523ed --- /dev/null +++ b/packages/vitec/Classes/DataProcessing/.msys00000000000000004dd18ef4cfaf2af9 @@ -0,0 +1,233 @@ + [ rendererClass, jsonKey ]. + * The renderer's renderForRecord() is invoked with the child's own + * tt_content row so the result is record-accurate (works with multiple + * containers / plugins on the same page). + */ + private const PLUGIN_RENDERERS = [ + 'vitec_productlist' => [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], + ]; + + public function process( + ContentObjectRenderer $cObj, + array $contentObjectConfiguration, + array $processorConfiguration, + array $processedData + ): array { + $as = (string)($processorConfiguration['as'] ?? 'items'); + + try { + $parentUid = (int)($cObj->data['uid'] ?? 0); + if ($parentUid <= 0) { + $processedData[$as] = []; + return $processedData; + } + + $pid = (int)($cObj->data['pid'] ?? 0); + $sysLanguageUid = (int)($cObj->data['sys_language_uid'] ?? 0); + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $rows = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('tx_container_parent', $qb->createNamedParameter($parentUid, Connection::PARAM_INT)), + $qb->expr()->eq('pid', $qb->createNamedParameter($pid, Connection::PARAM_INT)), + $qb->expr()->eq('sys_language_uid', $qb->createNamedParameter($sysLanguageUid, Connection::PARAM_INT)) + ) + ->orderBy('colPos') + ->addOrderBy('sorting') + ->executeQuery() + ->fetchAllAssociative(); + + $byColPos = []; + foreach ($rows as $record) { + $byColPos[(int)$record['colPos']][] = $this->normalise($record); + } + ksort($byColPos); + + $items = []; + foreach ($byColPos as $colPos => $contentElements) { + $items[] = [ + 'config' => ['colPos' => $colPos], + 'contentElements' => $contentElements, + ]; + } + + $processedData[$as] = $items; + } catch (\Throwable $e) { + $processedData[$as] = []; + } + + return $processedData; + } + + private function normalise(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if ($this->isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = $this->castValue($field, $value); + } + + // Resolve nested VITEC list-plugins to their headless JSON. + $this->resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)$record['colPos'], + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * If the child is a VITEC list-plugin, run its JSON renderer for THIS + * record and inject the decoded result under its key. Drops the raw + * pi_flexform XML afterwards. Never throws. + * + * @param array $record + * @param array $data + */ + private function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + + // The raw FlexForm XML is noise once the plugin is resolved. + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // Leave the raw data untouched on any failure. + } + } + + private function isEmpty(mixed $value): bool + { + return $value === null + || $value === '' + || $value === 0 + || $value === '0'; + } + + private function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php old mode 100755 new mode 100644 index cbb8eb0..380ab01 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -7,6 +7,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer; use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer; +use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer; +use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; +use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; +use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; +use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -70,6 +75,11 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], + 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], ]; public function process( @@ -171,13 +181,17 @@ final class ContainerChildrenProcessor implements DataProcessorInterface */ private function resolvePluginData(array $record, array &$data): void { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); $listType = (string)($record['list_type'] ?? ''); - if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) { + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { return; } try { - [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType]; + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; $renderer = GeneralUtility::makeInstance($rendererClass); if (!method_exists($renderer, 'renderForRecord')) { 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/Model/Product.php.bak.20260529111828 b/packages/vitec/Classes/Domain/Model/Product.php.bak.20260529111828 deleted file mode 100755 index a913f06..0000000 --- a/packages/vitec/Classes/Domain/Model/Product.php.bak.20260529111828 +++ /dev/null @@ -1,922 +0,0 @@ - - */ - protected $categories; - - /** - * Product images - * - * @var ObjectStorage - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $productimage; - - /** - * Downloads - * - * @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $downloads; - - - /** - * Open Graph Image - * - * @var FileReference - */ - protected $ogimage; - - /** - * Content element link - Key Features - * - * @var string - */ - protected $contentelement; - - /** - * Content element link - CTA Features - * - * @var string - */ - protected $contentelementcta; - - /** - * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Evomedien\Vitec\Domain\Model\Product> - * @TYPO3\CMS\Extbase\Annotation\ORM\Lazy - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $relatedprodukt; - - -/* --------------------------------------------------------------------- */ - - /** - * Returns the title - * - * @return string - */ - public function getTitle() - { - return $this->title; - } - - /** - * Sets the title - * - * @param string $title - * @return void - */ - public function setTitle(string $title) - { - $this->title = $title; - } - - /** - * Returns the video - * - * @return string - */ - public function getVideo() - { - return $this->video; - } - - /** - * Sets the video - * - * @param string $video - * @return void - */ - public function setVideo(string $video) - { - $this->video = $video; - } - - /** - * Returns the slug - * - * @return string - */ - public function getSlug() - { - return $this->slug; - } - - /** - * Sets the slug - * - * @param string $slug - * @return void - */ - public function setSlug(string $slug) - { - $this->slug = $slug; - } - - /** - * Returns the urltitle - * - * @return string - */ - public function getUrltitle() - { - return $this->urltitle; - } - - /** - * Sets the urltitle - * - * @param string $urltitle - * @return void - */ - public function setUrltitle(string $urltitle) - { - $this->urltitle = $urltitle; - } - - - /** - * Returns the seotitle - * - * @return string - */ - public function getSeotitle() - { - return $this->seotitle; - } - - /** - * Sets the seotitle - * - * @param string $seotitle - * @return void - */ - public function setSeotitle(string $seotitle) - { - $this->seotitle = $seotitle; - } - - - /** - * Returns the seometa - * - * @return string $seometa - */ - public function getSeometa() - { - return $this->seometa; - } - - /** - * Sets the seometa - * - * @param string $seometa - * @return void - */ - public function setSeometa($seometa) - { - $this->seometa = $seometa; - } - - /** - * Returns the keywords - * - * @return string $keywords - */ - public function getKeywords() - { - return $this->keywords; - } - - /** - * Sets the keywords - * - * @param string $keywords - * @return void - */ - public function setKeywords($keywords) - { - $this->keywords = $keywords; - } - - /** - * Returns the teaser - * - * @return string $kteasereywords - */ - public function getTeaser() - { - return $this->teaser; - } - - /** - * Sets the teaser - * - * @param string $teaser - * @return void - */ - public function setTeaser($teaser) - { - $this->teaser = $teaser; - } - - /** - * Returns the subtitle - * - * @return string $subtitle - */ - public function getSubtitle() - { - return $this->subtitle; - } - - /** - * Sets the subtitle - * - * @param string $subtitle - * @return void - */ - public function setSubtitle($subtitle) - { - $this->subtitle = $subtitle; - } - - /** - * Returns the hideonapp - * - * @return bool $hideonapp - */ - public function getHideonapp() - { - return $this->hideonapp; - } - - /** - * Sets the hideonapp - * - * @param bool $hideonapp - * @return void - */ - public function setHideonapp($hideonapp) - { - $this->hideonapp = $hideonapp; - } - - /** - * Returns the description - * - * @return bool $description - */ - public function getDescription() - { - return $this->description; - } - - /** - * Sets the description - * - * @param bool $description - * @return void - */ - public function setDescriptionp($description) - { - $this->description = $description; - } - - /** - * Returns the hideonwebsite - * - * @return bool $hideonwebsite - */ - public function getHideonwebsite() - { - return $this->hideonwebsite; - } - - /** - * Sets the hideonwebsite - * - * @param bool $hideonwebsite - * @return void - */ - public function setHideonwebsite($hideonwebsite) - { - $this->hideonwebsite = $hideonwebsite; - } - - /** - * Returns the hideondatasheets - * - * @return bool $hideondatasheets - */ - public function getHideondatasheets() - { - return $this->hideondatasheets; - } - - /** - * Sets the hideondatasheets - * - * @param bool $hideondatasheets - * @return void - */ - public function setHideondatasheets($hideondatasheets) - { - $this->hideondatasheets = $hideondatasheets; - } - - /** - * Returns the hideonproducts - * - * @return bool $hideonproducts - */ - public function getHideonproducts() - { - return $this->hideonproducts; - } - - /** - * Sets the hideonproducts - * - * @param bool $hideonproducts - * @return void - */ - public function setHideonproducts($hideonproducts) - { - $this->hideonproducts = $hideonproducts; - } - - /** - * Returns the structureddata - * - * @return string $structureddata - */ - public function getStructureddata() - { - return $this->structureddata; - } - - /** - * Sets the structureddata - * - * @param string $structureddata - * @return void - */ - public function setStructureddata($structureddata) - { - $this->structureddata = $structureddata; - } - - /** - * Returns the applications - * - * @return string $applications - */ - public function getApplications() - { - return $this->applications; - } - - /** - * Sets the applications - * - * @param string $applications - * @return void - */ - public function setApplications($applications) - { - $this->applications = $applications; - } - - /** - * Returns the highlights - * - * @return string $highlights - */ - public function getHighlights() - { - return $this->highlights; - } - - /** - * Sets the highlights - * - * @param string $highlights - * @return void - */ - public function setHighlights($highlights) - { - $this->highlights = $highlights; - } - /** - * Returns the shortcutpid - * - * @return string - */ - public function getShortcutpid() - { - return $this->shortcutpid; - } - - /** - * Sets the shortcutpid - * - * @param string $shortcutpid - * @return void - */ - public function setShortcutpid ($shortcutpid) - { - $this->shortcutpid = $shortcutpid; - } - - /** - * Returns the shortcut - * - * @return bool $shortcut - */ - public function getShortcut() - { - return $this->shortcut; - } - - /** - * Sets the shortcut - * - * @param bool $shortcut - * @return void - */ - public function setShortcut($shortcut) - { - $this->shortcut = $shortcut; - } - - /** - * Returns the legacy - * - * @return bool $legacy - */ - public function getLegacy() - { - return $this->legacy; - } - - /** - * Sets the legacy - * - * @param bool $legacy - * @return void - */ - public function setLegacy($legacy) - { - $this->legacy = $legacy; - } - - /** - * Returns the supportproduct - * - * @return bool $supportproduct - */ - public function getSupportproduct() - { - return $this->supportproduct; - } - - /** - * Sets the supportproduct - * - * @param bool $supportproduct - * @return void - */ - public function setSupportproduct($supportproduct) - { - $this->supportproduct = $supportproduct; - } - - /** - * Returns the subproduct - * - * @return bool $subproduct - */ - public function getSubproduct() - { - return $this->subproduct; - } - - /** - * Sets the subproduct - * - * @param bool $subproduct - * @return void - */ - public function setSubproduct($subproduct) - { - $this->subproduct = $subproduct; - } - - /** - * Initializes the ObjectStorage for categories - */ - public function __construct() - { - $this->categories = new ObjectStorage(); - $this->productimage = new ObjectStorage(); - $this->downloads = new ObjectStorage(); - } - - /** - * Adds a category - * - * @param Category $category - * @return void - */ - public function addCategory(Category $category) - { - $this->categories->attach($category); - } - - /** - * Removes a category - * - * @param Category $categoryToRemove - * @return void - */ - public function removeCategory(Category $categoryToRemove) - { - $this->categories->detach($categoryToRemove); - } - - /** - * Returns the categories - * - * @return ObjectStorage - */ - public function getCategories() - { - return $this->categories; - } - - /** - * Sets the categories - * - * @param ObjectStorage $categories - * @return void - */ - public function setCategories(ObjectStorage $categories) - { - $this->categories = $categories; - } - - - /** - * Adds a product image - * - * @param FileReference $productimage - * @return void - */ - public function addProductimage(FileReference $productimage) - { - $this->productimage->attach($productimage); - } - - /** - * Removes a product image - * - * @param FileReference $productimageToRemove - * @return void - */ - public function removeProductimage(FileReference $productimageToRemove) - { - $this->productimage->detach($productimageToRemove); - } - - /** - * Returns the product images - * - * @return ObjectStorage - */ - public function getProductimage() - { - return $this->productimage; - } - - /** - * Sets the product images - * - * @param ObjectStorage $productimage - * @return void - */ - public function setProductimage(ObjectStorage $productimage) - { - $this->productimage = $productimage; - } - - - /** - * Adds a download - * - * @param \Evomedien\Vitec\Domain\Model\Download $download - * @return void - */ - public function addDownload(\Evomedien\Vitec\Domain\Model\Download $download): void - { - $this->downloads->attach($download); - } - - /** - * Removes a download - * - * @param \Evomedien\Vitec\Domain\Model\Download $downloadToRemove - * @return void - */ - public function removeDownload(\Evomedien\Vitec\Domain\Model\Download $downloadToRemove): void - { - $this->downloads->detach($downloadToRemove); - } - - /** - * Returns the downloads - * - * @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> - */ - public function getDownloads(): ObjectStorage - { - return $this->downloads; - } - - /** - * Sets the downloads - * - * @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $downloads - * @return void - */ - public function setDownloads(ObjectStorage $downloads): void - { - $this->downloads = $downloads; - } - - - /** - * Returns the Open Graph Image - * - * @return FileReference|null - */ - public function getOgimage(): ?FileReference - { - return $this->ogimage; - } - - /** - * Sets the Open Graph Image - * - * @param FileReference $ogimage - * @return void - */ - public function setOgimage(FileReference $ogimage): void - { - $this->ogimage = $ogimage; - } - - /** - * Returns the content element link - * - * @return string - */ - public function getContentelement(): string - { - return $this->contentelement; - } - - /** - * Sets the content element link - * - * @param string $contentelement - * @return void - */ - public function setContentelement(string $contentelement): void - { - $this->contentelement = $contentelement; - } - - /** - * Returns the content element cta link - * - * @return string - */ - public function getContentelementcta(): string - { - return $this->contentelementcta; - } - - /** - * Sets the content element link cta - * - * @param string $contentelementcta - * @return void - */ - public function setContentelementcta(string $contentelementcta): void - { - $this->contentelementcta = $contentelementcta; - } - - /** - * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> - */ - public function getRelatedprodukt(): \TYPO3\CMS\Extbase\Persistence\ObjectStorage - { - return $this->relatedprodukt; - } - - /** - * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> $relatedprodukt - */ - public function setRelatedprodukt(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $relatedprodukt): void - { - $this->relatedprodukt = $relatedprodukt; - } -/* --------------------------------------------------------------------- */ - -} \ No newline at end of file diff --git a/packages/vitec/Classes/Domain/Model/Product.php.bak.20260529120751 b/packages/vitec/Classes/Domain/Model/Product.php.bak.20260529120751 deleted file mode 100755 index 54a7ae8..0000000 --- a/packages/vitec/Classes/Domain/Model/Product.php.bak.20260529120751 +++ /dev/null @@ -1,952 +0,0 @@ - - */ - protected $categories; - - /** - * Product images - * - * @var ObjectStorage - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $productimage; - - /** - * Downloads - * - * @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $downloads; - - - /** - * Open Graph Image - * - * @var FileReference - */ - protected $ogimage; - - /** - * Content element link - Key Features - * - * @var string - */ - - /** - * Uploaded / locally selected video file (FAL) - * - * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference|null - */ - protected $videofile; - - protected $contentelement; - - /** - * Content element link - CTA Features - * - * @var string - */ - protected $contentelementcta; - - /** - * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Evomedien\Vitec\Domain\Model\Product> - * @TYPO3\CMS\Extbase\Annotation\ORM\Lazy - * @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") - */ - protected $relatedprodukt; - - -/* --------------------------------------------------------------------- */ - - /** - * Returns the title - * - * @return string - */ - public function getTitle() - { - return $this->title; - } - - /** - * Sets the title - * - * @param string $title - * @return void - */ - public function setTitle(string $title) - { - $this->title = $title; - } - - /** - * Returns the video - * - * @return string - */ - public function getVideo() - { - return $this->video; - } - - /** - * Sets the video - * - * @param string $video - * @return void - */ - public function setVideo(string $video) - { - $this->video = $video; - } - - /** - * Returns the slug - * - * @return string - */ - public function getSlug() - { - return $this->slug; - } - - /** - * Sets the slug - * - * @param string $slug - * @return void - */ - public function setSlug(string $slug) - { - $this->slug = $slug; - } - - /** - * Returns the urltitle - * - * @return string - */ - public function getUrltitle() - { - return $this->urltitle; - } - - /** - * Sets the urltitle - * - * @param string $urltitle - * @return void - */ - public function setUrltitle(string $urltitle) - { - $this->urltitle = $urltitle; - } - - - /** - * Returns the seotitle - * - * @return string - */ - public function getSeotitle() - { - return $this->seotitle; - } - - /** - * Sets the seotitle - * - * @param string $seotitle - * @return void - */ - public function setSeotitle(string $seotitle) - { - $this->seotitle = $seotitle; - } - - - /** - * Returns the seometa - * - * @return string $seometa - */ - public function getSeometa() - { - return $this->seometa; - } - - /** - * Sets the seometa - * - * @param string $seometa - * @return void - */ - public function setSeometa($seometa) - { - $this->seometa = $seometa; - } - - /** - * Returns the keywords - * - * @return string $keywords - */ - public function getKeywords() - { - return $this->keywords; - } - - /** - * Sets the keywords - * - * @param string $keywords - * @return void - */ - public function setKeywords($keywords) - { - $this->keywords = $keywords; - } - - /** - * Returns the teaser - * - * @return string $kteasereywords - */ - public function getTeaser() - { - return $this->teaser; - } - - /** - * Sets the teaser - * - * @param string $teaser - * @return void - */ - public function setTeaser($teaser) - { - $this->teaser = $teaser; - } - - /** - * Returns the subtitle - * - * @return string $subtitle - */ - public function getSubtitle() - { - return $this->subtitle; - } - - /** - * Sets the subtitle - * - * @param string $subtitle - * @return void - */ - public function setSubtitle($subtitle) - { - $this->subtitle = $subtitle; - } - - /** - * Returns the hideonapp - * - * @return bool $hideonapp - */ - public function getHideonapp() - { - return $this->hideonapp; - } - - /** - * Sets the hideonapp - * - * @param bool $hideonapp - * @return void - */ - public function setHideonapp($hideonapp) - { - $this->hideonapp = $hideonapp; - } - - /** - * Returns the description - * - * @return bool $description - */ - public function getDescription() - { - return $this->description; - } - - /** - * Sets the description - * - * @param bool $description - * @return void - */ - public function setDescriptionp($description) - { - $this->description = $description; - } - - /** - * Returns the hideonwebsite - * - * @return bool $hideonwebsite - */ - public function getHideonwebsite() - { - return $this->hideonwebsite; - } - - /** - * Sets the hideonwebsite - * - * @param bool $hideonwebsite - * @return void - */ - public function setHideonwebsite($hideonwebsite) - { - $this->hideonwebsite = $hideonwebsite; - } - - /** - * Returns the hideondatasheets - * - * @return bool $hideondatasheets - */ - public function getHideondatasheets() - { - return $this->hideondatasheets; - } - - /** - * Sets the hideondatasheets - * - * @param bool $hideondatasheets - * @return void - */ - public function setHideondatasheets($hideondatasheets) - { - $this->hideondatasheets = $hideondatasheets; - } - - /** - * Returns the hideonproducts - * - * @return bool $hideonproducts - */ - public function getHideonproducts() - { - return $this->hideonproducts; - } - - /** - * Sets the hideonproducts - * - * @param bool $hideonproducts - * @return void - */ - public function setHideonproducts($hideonproducts) - { - $this->hideonproducts = $hideonproducts; - } - - /** - * Returns the structureddata - * - * @return string $structureddata - */ - public function getStructureddata() - { - return $this->structureddata; - } - - /** - * Sets the structureddata - * - * @param string $structureddata - * @return void - */ - public function setStructureddata($structureddata) - { - $this->structureddata = $structureddata; - } - - /** - * Returns the applications - * - * @return string $applications - */ - public function getApplications() - { - return $this->applications; - } - - /** - * Sets the applications - * - * @param string $applications - * @return void - */ - public function setApplications($applications) - { - $this->applications = $applications; - } - - /** - * Returns the highlights - * - * @return string $highlights - */ - public function getHighlights() - { - return $this->highlights; - } - - /** - * Sets the highlights - * - * @param string $highlights - * @return void - */ - public function setHighlights($highlights) - { - $this->highlights = $highlights; - } - /** - * Returns the shortcutpid - * - * @return string - */ - public function getShortcutpid() - { - return $this->shortcutpid; - } - - /** - * Sets the shortcutpid - * - * @param string $shortcutpid - * @return void - */ - public function setShortcutpid ($shortcutpid) - { - $this->shortcutpid = $shortcutpid; - } - - /** - * Returns the shortcut - * - * @return bool $shortcut - */ - public function getShortcut() - { - return $this->shortcut; - } - - /** - * Sets the shortcut - * - * @param bool $shortcut - * @return void - */ - public function setShortcut($shortcut) - { - $this->shortcut = $shortcut; - } - - /** - * Returns the legacy - * - * @return bool $legacy - */ - public function getLegacy() - { - return $this->legacy; - } - - /** - * Sets the legacy - * - * @param bool $legacy - * @return void - */ - public function setLegacy($legacy) - { - $this->legacy = $legacy; - } - - /** - * Returns the supportproduct - * - * @return bool $supportproduct - */ - public function getSupportproduct() - { - return $this->supportproduct; - } - - /** - * Sets the supportproduct - * - * @param bool $supportproduct - * @return void - */ - public function setSupportproduct($supportproduct) - { - $this->supportproduct = $supportproduct; - } - - /** - * Returns the subproduct - * - * @return bool $subproduct - */ - public function getSubproduct() - { - return $this->subproduct; - } - - /** - * Sets the subproduct - * - * @param bool $subproduct - * @return void - */ - public function setSubproduct($subproduct) - { - $this->subproduct = $subproduct; - } - - /** - * Initializes the ObjectStorage for categories - */ - public function __construct() - { - $this->categories = new ObjectStorage(); - $this->productimage = new ObjectStorage(); - $this->downloads = new ObjectStorage(); - } - - /** - * Adds a category - * - * @param Category $category - * @return void - */ - public function addCategory(Category $category) - { - $this->categories->attach($category); - } - - /** - * Removes a category - * - * @param Category $categoryToRemove - * @return void - */ - public function removeCategory(Category $categoryToRemove) - { - $this->categories->detach($categoryToRemove); - } - - /** - * Returns the categories - * - * @return ObjectStorage - */ - public function getCategories() - { - return $this->categories; - } - - /** - * Sets the categories - * - * @param ObjectStorage $categories - * @return void - */ - public function setCategories(ObjectStorage $categories) - { - $this->categories = $categories; - } - - - /** - * Adds a product image - * - * @param FileReference $productimage - * @return void - */ - public function addProductimage(FileReference $productimage) - { - $this->productimage->attach($productimage); - } - - /** - * Removes a product image - * - * @param FileReference $productimageToRemove - * @return void - */ - public function removeProductimage(FileReference $productimageToRemove) - { - $this->productimage->detach($productimageToRemove); - } - - /** - * Returns the product images - * - * @return ObjectStorage - */ - public function getProductimage() - { - return $this->productimage; - } - - /** - * Sets the product images - * - * @param ObjectStorage $productimage - * @return void - */ - public function setProductimage(ObjectStorage $productimage) - { - $this->productimage = $productimage; - } - - - /** - * Adds a download - * - * @param \Evomedien\Vitec\Domain\Model\Download $download - * @return void - */ - public function addDownload(\Evomedien\Vitec\Domain\Model\Download $download): void - { - $this->downloads->attach($download); - } - - /** - * Removes a download - * - * @param \Evomedien\Vitec\Domain\Model\Download $downloadToRemove - * @return void - */ - public function removeDownload(\Evomedien\Vitec\Domain\Model\Download $downloadToRemove): void - { - $this->downloads->detach($downloadToRemove); - } - - /** - * Returns the downloads - * - * @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> - */ - public function getDownloads(): ObjectStorage - { - return $this->downloads; - } - - /** - * Sets the downloads - * - * @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $downloads - * @return void - */ - public function setDownloads(ObjectStorage $downloads): void - { - $this->downloads = $downloads; - } - - - /** - * Returns the Open Graph Image - * - * @return FileReference|null - */ - public function getOgimage(): ?FileReference - { - return $this->ogimage; - } - - /** - * Sets the Open Graph Image - * - * @param FileReference $ogimage - * @return void - */ - public function setOgimage(FileReference $ogimage): void - { - $this->ogimage = $ogimage; - } - - /** - * Returns the content element link - * - * @return string - */ - - /** - * Returns the uploaded video file - * - * @return \TYPO3\CMS\Extbase\Domain\Model\FileReference|null - */ - public function getVideofile(): ?\TYPO3\CMS\Extbase\Domain\Model\FileReference - { - return $this->videofile; - } - - /** - * Sets the uploaded video file - * - * @param \TYPO3\CMS\Extbase\Domain\Model\FileReference|null $videofile - * @return void - */ - public function setVideofile(?\TYPO3\CMS\Extbase\Domain\Model\FileReference $videofile): void - { - $this->videofile = $videofile; - } - - public function getContentelement(): string - { - return $this->contentelement; - } - - /** - * Sets the content element link - * - * @param string $contentelement - * @return void - */ - public function setContentelement(string $contentelement): void - { - $this->contentelement = $contentelement; - } - - /** - * Returns the content element cta link - * - * @return string - */ - public function getContentelementcta(): string - { - return $this->contentelementcta; - } - - /** - * Sets the content element link cta - * - * @param string $contentelementcta - * @return void - */ - public function setContentelementcta(string $contentelementcta): void - { - $this->contentelementcta = $contentelementcta; - } - - /** - * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> - */ - public function getRelatedprodukt(): \TYPO3\CMS\Extbase\Persistence\ObjectStorage - { - return $this->relatedprodukt; - } - - /** - * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> $relatedprodukt - */ - public function setRelatedprodukt(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $relatedprodukt): void - { - $this->relatedprodukt = $relatedprodukt; - } -/* --------------------------------------------------------------------- */ - -} 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 new file mode 100644 index 0000000..a9f4fa0 --- /dev/null +++ b/packages/vitec/Classes/Service/.msys00000000000000002272de42bf87f8e5 @@ -0,0 +1,260 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/ContentElementResolver.php.bak.20260529120325 b/packages/vitec/Classes/Service/.msys0000000000000000274fb505bf117200 old mode 100755 new mode 100644 similarity index 83% rename from packages/vitec/Classes/Service/ContentElementResolver.php.bak.20260529120325 rename to packages/vitec/Classes/Service/.msys0000000000000000274fb505bf117200 index 09c7251..ea742dd --- a/packages/vitec/Classes/Service/ContentElementResolver.php.bak.20260529120325 +++ b/packages/vitec/Classes/Service/.msys0000000000000000274fb505bf117200 @@ -68,10 +68,12 @@ final class ContentElementResolver /** * Resolve a typolink string to a normalised tt_content JSON element. * - * Accepted forms: + * Accepted forms (all may include an optional anchor fragment): * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) * - "" (legacy: bare tt_content uid) - * - everything else → null (page links etc.) + * - everything else → null (pure page links etc.) * * @return array|null */ @@ -155,33 +157,47 @@ final class ContentElementResolver /** * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) */ private static function extractTtContentUid(string $link): int { + // Form 1: bare numeric uid if (ctype_digit($link)) { return (int)$link; } - if (!str_starts_with($link, 't3://record')) { - return 0; - } - $parts = parse_url($link); - if (empty($parts['query'])) { + if (!is_array($parts)) { return 0; } - $params = []; - parse_str((string)$parts['query'], $params); - - $identifier = (string)($params['identifier'] ?? ''); - $uid = (int)($params['uid'] ?? 0); - - if ($identifier !== 'tt_content' || $uid <= 0) { - return 0; + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } } - return $uid; + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; } /** diff --git a/packages/vitec/Classes/Service/.msys00000000000000002751b67b19a9c636 b/packages/vitec/Classes/Service/.msys00000000000000002751b67b19a9c636 new file mode 100644 index 0000000..1d957b6 --- /dev/null +++ b/packages/vitec/Classes/Service/.msys00000000000000002751b67b19a9c636 @@ -0,0 +1,256 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + $listType = (string)($record['list_type'] ?? ''); + if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/.msys00000000000000002752b73c4a27d4ba b/packages/vitec/Classes/Service/.msys00000000000000002752b73c4a27d4ba new file mode 100644 index 0000000..09993fc --- /dev/null +++ b/packages/vitec/Classes/Service/.msys00000000000000002752b73c4a27d4ba @@ -0,0 +1,260 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/.msys00000000000000002753b7f777b58735 b/packages/vitec/Classes/Service/.msys00000000000000002753b7f777b58735 new file mode 100644 index 0000000..1e7b9c3 --- /dev/null +++ b/packages/vitec/Classes/Service/.msys00000000000000002753b7f777b58735 @@ -0,0 +1,262 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/.msys00000000000000002753b7f978b2a67b b/packages/vitec/Classes/Service/.msys00000000000000002753b7f978b2a67b new file mode 100644 index 0000000..bfad1e3 --- /dev/null +++ b/packages/vitec/Classes/Service/.msys00000000000000002753b7f978b2a67b @@ -0,0 +1,264 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/.msys0000000000000000284b115dd4770044 b/packages/vitec/Classes/Service/.msys0000000000000000284b115dd4770044 new file mode 100644 index 0000000..26a0674 --- /dev/null +++ b/packages/vitec/Classes/Service/.msys0000000000000000284b115dd4770044 @@ -0,0 +1,266 @@ + [ProductListJsonRenderer::class, 'products'], + 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], + 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], + 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], + ]; + + /** + * Resolve a typolink string to a normalised tt_content JSON element. + * + * Accepted forms (all may include an optional anchor fragment): + * - "t3://record?identifier=tt_content&uid=N" + * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; + * the fragment is the tt_content uid) + * - "" (legacy: bare tt_content uid) + * - everything else → null (pure page links etc.) + * + * @return array|null + */ + public static function resolveLink(?string $link): ?array + { + try { + $link = trim((string)$link); + if ($link === '') { + return null; + } + + $uid = self::extractTtContentUid($link); + if ($uid <= 0) { + return null; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + $row = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return self::normaliseRecord($row); + } catch (\Throwable $e) { + return null; + } + } + + /** + * Normalise a tt_content DB row to the same envelope shape that + * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} + * emits for container children. VITEC list-plugin children are + * resolved to their headless JSON. + * + * @param array $record + * @return array + */ + public static function normaliseRecord(array $record): array + { + $data = []; + foreach ($record as $field => $value) { + if (in_array($field, self::ENVELOPE, true)) { + continue; + } + if (in_array($field, self::SYSTEM_FIELDS, true)) { + continue; + } + if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { + continue; + } + $data[$field] = self::castValue($field, $value); + } + + self::resolvePluginData($record, $data); + + return [ + 'id' => (int)$record['uid'], + 'type' => (string)$record['CType'], + 'colPos' => (int)($record['colPos'] ?? 0), + 'sorting' => (int)($record['sorting'] ?? 0), + 'appearance' => [ + 'layout' => (string)($record['layout'] ?? ''), + 'frameClass' => (string)($record['frame_class'] ?? 'default'), + 'spaceBefore' => (string)($record['space_before_class'] ?? ''), + 'spaceAfter' => (string)($record['space_after_class'] ?? ''), + ], + 'data' => (object)$data, + ]; + } + + /** + * Extract the tt_content uid from a typolink string. + * + * Handles four forms: + * 1. plain numeric → tt_content uid + * 2. t3://record?identifier=tt_content&uid=N → uid N + * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) + * 4. t3://page?uid=PAGE#N → uid N from fragment + * (link-popup picks a content element on a page; the fragment is + * the tt_content uid, the query is the page uid) + */ + private static function extractTtContentUid(string $link): int + { + // Form 1: bare numeric uid + if (ctype_digit($link)) { + return (int)$link; + } + + $parts = parse_url($link); + if (!is_array($parts)) { + return 0; + } + + // Forms 2 & 3: t3://record?identifier=tt_content&uid=N + if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { + $params = []; + parse_str((string)$parts['query'], $params); + $identifier = (string)($params['identifier'] ?? ''); + $uid = (int)($params['uid'] ?? 0); + if ($identifier === 'tt_content' && $uid > 0) { + return $uid; + } + } + + // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment + if (str_starts_with($link, 't3://page')) { + $fragment = (string)($parts['fragment'] ?? ''); + if (ctype_digit($fragment)) { + return (int)$fragment; + } + } + + return 0; + } + + /** + * If the record is a VITEC list-plugin, run its renderer for THIS row + * and inject the decoded result under its key. Drops raw pi_flexform. + * + * @param array $record + * @param array $data + */ + private static function resolvePluginData(array $record, array &$data): void + { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); + $listType = (string)($record['list_type'] ?? ''); + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { + return; + } + + try { + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; + $renderer = GeneralUtility::makeInstance($rendererClass); + + if (!method_exists($renderer, 'renderForRecord')) { + return; + } + + $json = $renderer->renderForRecord($record); + if ($json === '' || $json === null) { + return; + } + + $decoded = json_decode($json, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return; + } + + $data[$jsonKey] = $decoded; + unset($data['pi_flexform']); + } catch (\Throwable $e) { + // leave raw data on failure + } + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === '' || $value === 0 || $value === '0'; + } + + private static function castValue(string $field, mixed $value): mixed + { + if (in_array($field, ['header_layout'], true)) { + return (int)$value; + } + return $value; + } +} diff --git a/packages/vitec/Classes/Service/ContentElementResolver.php b/packages/vitec/Classes/Service/ContentElementResolver.php old mode 100755 new mode 100644 index ea742dd..61c7b96 --- a/packages/vitec/Classes/Service/ContentElementResolver.php +++ b/packages/vitec/Classes/Service/ContentElementResolver.php @@ -9,6 +9,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer; use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer; +use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer; +use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; +use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; +use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; +use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -63,6 +68,11 @@ final class ContentElementResolver 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], + 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], + 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], + 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], + 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], + 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], ]; /** @@ -209,13 +219,17 @@ final class ContentElementResolver */ private static function resolvePluginData(array $record, array &$data): void { + // v14: plugins are their own CType; legacy elements still carry list_type. + $cType = (string)($record['CType'] ?? ''); $listType = (string)($record['list_type'] ?? ''); - if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) { + $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType + : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); + if ($key === null) { return; } try { - [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType]; + [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; $renderer = GeneralUtility::makeInstance($rendererClass); if (!method_exists($renderer, 'renderForRecord')) { diff --git a/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php b/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php new file mode 100644 index 0000000..d82c17b --- /dev/null +++ b/packages/vitec/Classes/UserFunc/DatasheetsJsonRenderer.php @@ -0,0 +1,395 @@ +getAttribute("frontend.page.information"); + if ($pi !== null) { $pageId = (int)$pi->getId(); } + } + if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $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_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('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($rows)) { + return ''; + } + + return $this->renderForRecord($rows[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'] ?? []; + + $debugMode = (bool)($settings['debug'] ?? false); + + $settingsOut = [ + 'showFilter' => (bool)($settings['showFilter'] ?? true), + 'showSearch' => (bool)($settings['showSearch'] ?? true), + 'itemsPerPage' => (int)($settings['itemsPerPage'] ?? 20), + ]; + + // 1) Pull all (product_uid, latest_datasheet_uid) pairs in one query. + // GROUP BY product, take MAX(tstamp) → join back to get that exact download. + $items = $this->fetchProductLatestDatasheetPairs(); + + // 2) Hydrate each pair: full product + full datasheet objects. + $itemsOut = []; + foreach ($items as $pair) { + $productUid = (int)$pair['product_uid']; + $datasheetUid = (int)$pair['datasheet_uid']; + + $product = $this->fetchProduct($productUid); + $datasheet = $this->fetchDownload($datasheetUid); + if (!$product || !$datasheet) { + continue; + } + + $itemsOut[] = [ + 'product' => $this->serializeProduct($product), + 'datasheet' => $this->serializeDatasheet($datasheet), + ]; + } + + $response = [ + 'items' => $itemsOut, + 'settings' => $settingsOut, + ]; + + if ($debugMode) { + $response['debug'] = [ + 'productCount' => count($itemsOut), + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * For each visible product that has at least one visible datasheet, + * return [product_uid => latest_datasheet_uid]. Sorted by product title. + * + * @return array + */ + private function fetchProductLatestDatasheetPairs(): array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_product_download_mm'); + $expr = $qb->expr(); + + // Inner aggregation: for each product, the latest download tstamp. + // We use a single query with JOIN on the same (mm + download) to resolve + // the actual download_uid matching MAX(tstamp). Approach: do it in two + // simple steps to stay SQL-portable. + + // Step A: gather all candidate (product_uid, download_uid, tstamp) tuples. + $rows = $qb + ->select( + 'mm.uid_local AS product_uid', + 'mm.uid_foreign AS datasheet_uid', + 'd.tstamp AS d_tstamp', + 'p.title AS p_title' + ) + ->from('tx_vitec_product_download_mm', 'mm') + ->join('mm', 'tx_vitec_domain_model_download', 'd', 'd.uid = mm.uid_foreign') + ->join('mm', 'tx_vitec_domain_model_product', 'p', 'p.uid = mm.uid_local') + ->where( + $expr->eq('d.deleted', 0), + $expr->eq('d.hidden', 0), + $expr->eq('d.hideonwebsite', 0), + $expr->eq('d.hideondatasheets', 0), + $expr->eq('p.deleted', 0), + $expr->eq('p.hidden', 0), + $expr->eq('p.hideonwebsite', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + // Step B: reduce to {product_uid => latest datasheet} in PHP. + $latestPerProduct = []; // product_uid => [datasheet_uid, tstamp, title] + foreach ($rows as $r) { + $pUid = (int)$r['product_uid']; + $dUid = (int)$r['datasheet_uid']; + $ts = (int)$r['d_tstamp']; + $title = (string)$r['p_title']; + + if (!isset($latestPerProduct[$pUid]) || $ts > $latestPerProduct[$pUid]['tstamp']) { + $latestPerProduct[$pUid] = [ + 'product_uid' => $pUid, + 'datasheet_uid' => $dUid, + 'tstamp' => $ts, + 'title' => $title, + ]; + } + } + + // Sort by product title ASC. + usort($latestPerProduct, static fn($a, $b) => strcasecmp($a['title'], $b['title'])); + + // Strip helper fields. + return array_map( + static fn($r) => ['product_uid' => $r['product_uid'], 'datasheet_uid' => $r['datasheet_uid']], + $latestPerProduct + ); + } + + /** + * @return array|false + */ + private function fetchProduct(int $uid) + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_product'); + return $qb + ->select('uid', 'title', 'slug', 'subtitle', 'teaser') + ->from('tx_vitec_domain_model_product') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + } + + /** + * @return array|false + */ + private function fetchDownload(int $uid) + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_download'); + return $qb + ->select('*') + ->from('tx_vitec_domain_model_download') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + } + + /** + * @param array $product + * @return array + */ + private function serializeProduct(array $product): array + { + $uid = (int)$product['uid']; + return [ + 'uid' => $uid, + 'title' => (string)($product['title'] ?? ''), + 'slug' => (string)($product['slug'] ?? ''), + 'subtitle' => (string)($product['subtitle'] ?? ''), + 'teaser' => (string)($product['teaser'] ?? ''), + 'link' => '/product/' . (string)($product['slug'] ?? ''), + 'image' => $this->getProductFirstImage($uid), + 'categories' => $this->getProductCategories($uid), + ]; + } + + /** + * @param array $download + * @return array + */ + private function serializeDatasheet(array $download): array + { + $uid = (int)$download['uid']; + return [ + 'uid' => $uid, + 'title' => (string)($download['title'] ?? ''), + 'slug' => (string)($download['slug'] ?? ''), + 'teaser' => (string)($download['teaser'] ?? ''), + 'description' => (string)($download['description'] ?? ''), + 'tstamp' => (int)($download['tstamp'] ?? 0), + 'file' => $this->getDownloadFile($uid), + ]; + } + + /** + * @return array|null + */ + private function getProductFirstImage(int $productUid): ?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_product', ParameterType::STRING)), + $qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter('productimage', ParameterType::STRING)), + $qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($productUid, 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']); + + $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'] ?? '', + ]; + } catch (\Exception $e) { + return null; + } + } + + private function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $qb->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($productUid, 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); + } + + /** + * @return array|null + */ + private function getDownloadFile(int $downloadUid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + + $row = $qb + ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid AS file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') + ->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_download', ParameterType::STRING)), + $qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)), + $qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('fr.deleted', 0), + $qb->expr()->eq('f.missing', 0) + ) + ->orderBy('fr.sorting_foreign', 'ASC') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return [ + 'uid' => (int)$row['file_uid'], + 'name' => (string)($row['name'] ?? ''), + 'url' => '/fileadmin' . ($row['identifier'] ?? ''), + 'size' => (int)($row['size'] ?? 0), + 'extension' => (string)($row['extension'] ?? ''), + 'mimeType' => (string)($row['mime_type'] ?? ''), + 'title' => (string)($row['title'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + ]; + } +} diff --git a/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php b/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php new file mode 100644 index 0000000..fd3983f --- /dev/null +++ b/packages/vitec/Classes/UserFunc/DownloadcardJsonRenderer.php @@ -0,0 +1,298 @@ + single download object (key: "download") + * - else settings.product set -> all downloads of that product (key: "downloads", + * plus a small "product" context) + * - neither -> empty (key removed by headless ifEmptyUnsetKey) + * + * `settings.layout` / magstyle / magheader / magtext / maglink / adddetaillink + * are always passed through under the "settings" block for the frontend. + * + * `render()` performs page discovery; `renderForRecord()` processes a specific + * tt_content row and is reused by ContainerChildrenProcessor / + * ContentElementResolver. Exception-safe. + */ +class DownloadcardJsonRenderer +{ + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + + $pageId = 0; + $req = $GLOBALS["TYPO3_REQUEST"] ?? null; + if ($req !== null) { + $pi = $req->getAttribute("frontend.page.information"); + if ($pi !== null) { $pageId = (int)$pi->getId(); } + } + if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); } + + $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('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($rows)) { + return ''; + } + + return $this->renderForRecord($rows[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'] ?? []; + + $downloadUid = (int)($settings['download'] ?? 0); + $productUid = (int)($settings['product'] ?? 0); + $debugMode = (bool)($settings['debug'] ?? false); + + // Settings always carried through to the frontend + $settingsOut = [ + 'layout' => (string)($settings['layout'] ?? ''), + 'magstyle' => (bool)($settings['magstyle'] ?? false), + 'magheader' => (string)($settings['magheader'] ?? ''), + 'magtext' => (string)($settings['magtext'] ?? ''), + 'maglink' => (string)($settings['maglink'] ?? ''), + 'adddetaillink' => (bool)($settings['adddetaillink'] ?? false), + ]; + + // --- Mode 1: single download --- + if ($downloadUid > 0) { + $download = $this->fetchDownload($downloadUid); + if (!$download) { + return $debugMode + ? json_encode(['error' => 'Download not found', 'debug' => ['downloadUid' => $downloadUid]]) + : ''; + } + + $response = [ + 'mode' => 'single', + 'download' => $this->serializeDownload($download), + 'settings' => $settingsOut, + ]; + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, 'downloadUid' => $downloadUid, 'settings' => $settings, + ]; + } + return json_encode($response); + } + + // --- Mode 2: all downloads of a product --- + if ($productUid > 0) { + $downloads = $this->fetchDownloadsForProduct($productUid); + $product = $this->fetchProductContext($productUid); + + $response = [ + 'mode' => 'product-downloads', + 'product' => $product, + 'downloads' => array_map(fn($r) => $this->serializeDownload($r), $downloads), + 'settings' => $settingsOut, + ]; + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, + 'productUid' => $productUid, + 'downloadCount' => count($downloads), + 'settings' => $settings, + ]; + } + return json_encode($response); + } + + // Neither selected: nothing to render. + return $debugMode + ? json_encode(['error' => 'No download or product selected', 'debug' => ['settings' => $settings]]) + : ''; + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @return array|false + */ + private function fetchDownload(int $uid) + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_download'); + + return $qb + ->select('*') + ->from('tx_vitec_domain_model_download') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0), + $qb->expr()->eq('hideonwebsite', 0) + ) + ->executeQuery() + ->fetchAssociative(); + } + + /** + * @return array> + */ + private function fetchDownloadsForProduct(int $productUid): array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_download'); + + return $qb + ->select('d.*') + ->from('tx_vitec_domain_model_download', 'd') + ->join('d', 'tx_vitec_product_download_mm', 'mm', 'mm.uid_foreign = d.uid') + ->where( + $qb->expr()->eq('mm.uid_local', $qb->createNamedParameter($productUid, ParameterType::INTEGER)), + $qb->expr()->eq('d.deleted', 0), + $qb->expr()->eq('d.hidden', 0), + $qb->expr()->eq('d.hideonwebsite', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * @return array|null + */ + private function fetchProductContext(int $productUid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_product'); + + $row = $qb + ->select('uid', 'title', 'slug', 'subtitle', 'teaser') + ->from('tx_vitec_domain_model_product') + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($productUid, 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'] ?? ''), + 'slug' => (string)($row['slug'] ?? ''), + 'subtitle' => (string)($row['subtitle'] ?? ''), + 'teaser' => (string)($row['teaser'] ?? ''), + 'link' => '/product/' . (string)($row['slug'] ?? ''), + ]; + } + + /** + * @param array $download + * @return array + */ + protected function serializeDownload(array $download): array + { + $uid = (int)$download['uid']; + + return [ + 'uid' => $uid, + 'title' => (string)($download['title'] ?? ''), + 'slug' => (string)($download['slug'] ?? ''), + 'teaser' => (string)($download['teaser'] ?? ''), + 'description' => (string)($download['description'] ?? ''), + 'keywords' => (string)($download['keywords'] ?? ''), + 'icon' => (string)($download['icon'] ?? ''), + 'filepath' => (string)($download['filepath'] ?? ''), + 'fileprefix' => (string)($download['fileprefix'] ?? ''), + '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), + ]; + } + + /** + * Resolve the FAL file reference (fieldname=file) for a download. + * + * @return array|null + */ + protected function getDownloadFile(int $downloadUid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + + $row = $qb + ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid AS file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') + ->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_download', ParameterType::STRING)), + $qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)), + $qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('fr.deleted', 0), + $qb->expr()->eq('f.missing', 0) + ) + ->orderBy('fr.sorting_foreign', 'ASC') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return [ + 'uid' => (int)$row['file_uid'], + 'name' => (string)($row['name'] ?? ''), + 'url' => '/fileadmin' . ($row['identifier'] ?? ''), + 'size' => (int)($row['size'] ?? 0), + 'extension' => (string)($row['extension'] ?? ''), + 'mimeType' => (string)($row['mime_type'] ?? ''), + 'title' => (string)($row['title'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + ]; + } +} diff --git a/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php b/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php new file mode 100644 index 0000000..c31e527 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/DownloadcardcollectionJsonRenderer.php @@ -0,0 +1,311 @@ +getAttribute("frontend.page.information"); + if ($pi !== null) { $pageId = (int)$pi->getId(); } + } + if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $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_downloadcardcollection', ParameterType::STRING)), + $qb->expr()->and( + $qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcardcollection', ParameterType::STRING)) + ) + ), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($rows)) { + return ''; + } + + return $this->renderForRecord($rows[0]); + } + + /** + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $ttContentUid = (int)($contentElement['uid'] ?? 0); + + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $debugMode = (bool)($settings['debug'] ?? false); + + // Pass-through settings for the frontend + $settingsOut = [ + 'layout' => (string)($settings['layout'] ?? ''), + 'magstyle' => (bool)($settings['magstyle'] ?? false), + 'magheader' => (string)($settings['magheader'] ?? ''), + 'magtext' => (string)($settings['magtext'] ?? ''), + 'maglink' => (string)($settings['maglink'] ?? ''), + 'adddetaillink' => (bool)($settings['adddetaillink'] ?? false), + ]; + + // --- Downloads: comma-separated UID list from settings.download --- + $downloadUids = $this->parseUidList($settings['download'] ?? ''); + $downloads = $downloadUids === [] ? [] : $this->fetchDownloadsByUids($downloadUids); + + // --- 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), + 'settings' => $settingsOut, + ]; + + if ($debugMode) { + $response['debug'] = [ + 'ttContentUid' => $ttContentUid, + 'downloadUids' => $downloadUids, + 'downloadCount' => count($downloads), + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * Accepts "5,7,12" or "5,,7" or even an array-like value. Returns int[]. + * + * @return int[] + */ + private function parseUidList(mixed $raw): array + { + if (is_array($raw)) { + $raw = implode(',', $raw); + } + $raw = (string)$raw; + if ($raw === '') { + return []; + } + $uids = array_filter(array_map('intval', explode(',', $raw)), static fn(int $u) => $u > 0); + return array_values(array_unique($uids)); + } + + /** + * @param int[] $uids + * @return array> + */ + private function fetchDownloadsByUids(array $uids): array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_download'); + + $rows = $qb + ->select('*') + ->from('tx_vitec_domain_model_download') + ->where( + $qb->expr()->in('uid', $qb->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0), + $qb->expr()->eq('hideonwebsite', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + // Preserve the editor-defined order from $uids + $orderMap = array_flip($uids); + usort($rows, static function ($a, $b) use ($orderMap) { + return ($orderMap[(int)$a['uid']] ?? PHP_INT_MAX) <=> ($orderMap[(int)$b['uid']] ?? PHP_INT_MAX); + }); + + return $rows; + } + + /** + * Resolve the inline FAL header image stored on this tt_content row + * (tablenames='tt_content', fieldname='image'). + * + * @return array|null + */ + private function getCollectionImage(int $ttContentUid): ?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('tt_content', ParameterType::STRING)), + $qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter('image', ParameterType::STRING)), + $qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($ttContentUid, 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, 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; + } + } + + /** + * @param array $download + * @return array + */ + protected function serializeDownload(array $download): array + { + $uid = (int)$download['uid']; + + return [ + 'uid' => $uid, + 'title' => (string)($download['title'] ?? ''), + 'slug' => (string)($download['slug'] ?? ''), + 'teaser' => (string)($download['teaser'] ?? ''), + 'description' => (string)($download['description'] ?? ''), + 'keywords' => (string)($download['keywords'] ?? ''), + 'icon' => (string)($download['icon'] ?? ''), + 'filepath' => (string)($download['filepath'] ?? ''), + 'fileprefix' => (string)($download['fileprefix'] ?? ''), + '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), + ]; + } + + /** + * Resolve the FAL file reference (fieldname=file) for a download. + * + * @return array|null + */ + protected function getDownloadFile(int $downloadUid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + + $row = $qb + ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid AS file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') + ->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_download', ParameterType::STRING)), + $qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)), + $qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), + $qb->expr()->eq('fr.deleted', 0), + $qb->expr()->eq('f.missing', 0) + ) + ->orderBy('fr.sorting_foreign', 'ASC') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$row) { + return null; + } + + return [ + 'uid' => (int)$row['file_uid'], + 'name' => (string)($row['name'] ?? ''), + 'url' => '/fileadmin' . ($row['identifier'] ?? ''), + 'size' => (int)($row['size'] ?? 0), + 'extension' => (string)($row['extension'] ?? ''), + 'mimeType' => (string)($row['mime_type'] ?? ''), + 'title' => (string)($row['title'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + ]; + } +} diff --git a/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php new file mode 100644 index 0000000..19a28e4 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php @@ -0,0 +1,270 @@ +cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_marketshow') { + 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) { + $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 ''; + } + + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $contentElements = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_marketshow', ParameterType::STRING)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->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 { + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $marketUid = (int)($settings['market'] ?? 0); + $layout = (string)($settings['layout'] ?? 'default'); + $debugMode = (bool)($settings['debug'] ?? false); + + // Fallback: route / query parameter (numeric uid only — Market has no slug) + if (!$marketUid) { + $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); + $marketParam = $routeParams['tx_vitec_marketshow']['market'] ?? null; + if ($marketParam && is_numeric($marketParam)) { + $marketUid = (int)$marketParam; + } + } + + if (!$marketUid) { + return $debugMode + ? json_encode(['error' => 'No market selected or found', 'debug' => ['settings' => $settings]]) + : ''; + } + + $marketQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_market'); + + $market = $marketQb + ->select('*') + ->from('tx_vitec_domain_model_market') + ->where( + $marketQb->expr()->eq('uid', $marketQb->createNamedParameter($marketUid, ParameterType::INTEGER)), + $marketQb->expr()->eq('deleted', 0), + $marketQb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$market) { + return $debugMode + ? json_encode(['error' => 'Market not found', 'debug' => ['marketUid' => $marketUid]]) + : ''; + } + + $response = [ + 'market' => $this->serializeMarket($market), + 'layout' => $layout, + 'settings' => [ + 'layout' => $layout, + ], + ]; + + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, + 'marketUid' => $marketUid, + 'layout' => $layout, + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @param array $market + * @return array + */ + protected function serializeMarket(array $market): array + { + $uid = (int)$market['uid']; + + return [ + 'uid' => $uid, + 'title' => (string)($market['title'] ?? ''), + 'subtitle' => (string)($market['subtitle'] ?? ''), + 'teaser' => (string)($market['teaser'] ?? ''), + 'description' => (string)($market['description'] ?? ''), + 'categories' => $this->getMarketCategories($uid), + 'image' => $this->getMarketImage($uid), + ]; + } + + /** + * Resolve the single image FAL reference for a market (fieldname=image). + * + * @return array|null + */ + protected function getMarketImage(int $marketUid): ?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_market', ParameterType::STRING)), + $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('image', ParameterType::STRING)), + $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($marketUid, 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 getMarketCategories(int $marketUid): 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_market', ParameterType::STRING) . + ' AND mm.fieldname = ' . + $queryBuilder->createNamedParameter('categories', ParameterType::STRING) + ) + ->where( + $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($marketUid, 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/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php old mode 100755 new mode 100644 index 4c085f3..0221e00 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; + +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; use Evomedien\Vitec\Domain\Repository\ProductRepository; use Psr\Http\Message\ServerRequestInterface; @@ -25,9 +27,30 @@ use TYPO3\CMS\Extbase\Service\ImageService; */ class ProductListJsonRenderer { + #[AsAllowedCallable] public function render(string $content, array $conf): string { - $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + // 1) cObj data path + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_productlist') { + 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) { + $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 ''; + } $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); @@ -37,7 +60,7 @@ class ProductListJsonRenderer ->from('tt_content') ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), $queryBuilder->expr()->eq('deleted', 0), $queryBuilder->expr()->eq('hidden', 0) ) @@ -45,13 +68,13 @@ class ProductListJsonRenderer ->fetchAllAssociative(); if (empty($contentElements)) { - // Not a product-list page: emit nothing so headless removes the key. return ''; } return $this->renderForRecord($contentElements[0]); } + /** * Render exactly the given tt_content row (the product-list plugin element). * Exception-safe: returns '' on any failure. diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518144047 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518144047 deleted file mode 100755 index 03f7556..0000000 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518144047 +++ /dev/null @@ -1,228 +0,0 @@ -id ?? 0); - - // Query tt_content for vitec_productlist on this page - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return json_encode(['debug' => 'No vitec_productlist on page ' . $pageId]); - } - - // Take the first one (there should typically be only one) - $contentElement = $contentElements[0]; - - // Parse FlexForm - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - // Extract category UIDs and debug setting - $categoryUids = array_filter( - array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) - ); - $debugMode = (bool)($settings['debug'] ?? false); - $allProducts = (bool)($settings['allproducts'] ?? false); - - // Extbase repositories don't work in UserFunc context - // Use direct database query instead - $productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productQuery = $productQueryBuilder - ->select('p.*') - ->from('tx_vitec_domain_model_product', 'p') - ->where( - $productQueryBuilder->expr()->eq('p.deleted', 0), - $productQueryBuilder->expr()->eq('p.hidden', 0), - $productQueryBuilder->expr()->eq('p.legacy', 0), - $productQueryBuilder->expr()->eq('p.supportproduct', 0), - $productQueryBuilder->expr()->eq('p.hideonwebsite', 0), - $productQueryBuilder->expr()->eq('p.hideonproducts', 0), - $productQueryBuilder->expr()->eq('p.subproduct', 0) - ); - - // Add category filter if specified and allProducts is not enabled - if (!empty($categoryUids) && !$allProducts) { - // Join with sys_category_record_mm to filter by categories - $productQuery - ->join( - 'p', - 'sys_category_record_mm', - 'mm', - 'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->andWhere( - $productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)) - ) - ->groupBy('p.uid'); - } - - $products = $productQuery->executeQuery()->fetchAllAssociative(); - - // Serialize products (they're already associative arrays from the query) - $productsData = []; - foreach ($products as $product) { - // Get product images (FAL references) - $images = $this->getProductImages((int)$product['uid']); - - $productsData[] = [ - 'uid' => (int)$product['uid'], - 'title' => $product['title'] ?? '', - 'subtitle' => $product['subtitle'] ?? '', - 'slug' => $product['slug'] ?? '', - 'teaser' => $product['teaser'] ?? '', - 'description' => $product['description'] ?? '', - 'link' => '/product/' . ($product['slug'] ?? ''), - 'images' => $images, - // Add more fields as needed - ]; - } - - // If debug mode is enabled, return object with products and debug info - if ($debugMode) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => $pageId, - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings - ] - ]); - } - - // Otherwise return products array directly (backward compatible) - return json_encode($productsData); - } - - /** - * Get product images from FAL (sys_file_reference) - * Processes images through ImageService and generates srcset for responsive images - */ - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('sfr.uid', 'sfr.uid_local', '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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->executeQuery() - ->fetchAllAssociative(); - - $imageService = GeneralUtility::makeInstance(ImageService::class); - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $images = []; - foreach ($fileReferences as $fileRefData) { - try { - // Get FAL FileReference object - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - // Define image sizes for srcset - $sizes = [ - 'small' => ['width' => 400, 'height' => null], - 'medium' => ['width' => 800, 'height' => null], - 'large' => ['width' => 1200, 'height' => null], - 'xlarge' => ['width' => 1600, 'height' => null], - ]; - - $srcset = []; - foreach ($sizes as $sizeName => $dimensions) { - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - [ - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'crop' => $fileRefData['crop'] ?? null - ] - ); - - $imageUri = $imageService->getImageUri($processedImage); - $srcset[] = [ - 'url' => $imageUri, - 'width' => $dimensions['width'], - 'descriptor' => $dimensions['width'] . 'w' - ]; - } - - // Get original/default image - $defaultProcessed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - $images[] = [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($defaultProcessed), - '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) { - // Skip images that can't be processed - continue; - } - } - - return $images; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 deleted file mode 100755 index a65264d..0000000 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 +++ /dev/null @@ -1,499 +0,0 @@ -id ?? 0); - - // Query tt_content for vitec_productlist on this page - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return json_encode(['debug' => 'No vitec_productlist on page ' . $pageId]); - } - - // Take the first one (there should typically be only one) - $contentElement = $contentElements[0]; - - // Parse FlexForm - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - // Extract category UIDs and debug setting - $categoryUids = array_filter( - array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) - ); - $debugMode = (bool)($settings['debug'] ?? false); - $allProducts = (bool)($settings['allproducts'] ?? false); - - // Extbase repositories don't work in UserFunc context - // Use direct database query instead - $productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productQuery = $productQueryBuilder - ->select('p.*') - ->from('tx_vitec_domain_model_product', 'p') - ->where( - $productQueryBuilder->expr()->eq('p.deleted', 0), - $productQueryBuilder->expr()->eq('p.hidden', 0), - $productQueryBuilder->expr()->eq('p.legacy', 0), - $productQueryBuilder->expr()->eq('p.supportproduct', 0), - $productQueryBuilder->expr()->eq('p.hideonwebsite', 0), - $productQueryBuilder->expr()->eq('p.hideonproducts', 0), - $productQueryBuilder->expr()->eq('p.subproduct', 0) - ); - - // Add category filter if specified and allProducts is not enabled - if (!empty($categoryUids) && !$allProducts) { - // Join with sys_category_record_mm to filter by categories - $productQuery - ->join( - 'p', - 'sys_category_record_mm', - 'mm', - 'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->andWhere( - $productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)) - ) - ->groupBy('p.uid'); - } - - $products = $productQuery->executeQuery()->fetchAllAssociative(); - - // Serialize products (they're already associative arrays from the query) - $productsData = []; - foreach ($products as $product) { - $productsData[] = $this->serializeProduct($product); - } - - // If debug mode is enabled, return object with products and debug info - if ($debugMode) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => $pageId, - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings - ] - ]); - } - - // Otherwise return products array directly (backward compatible) - return json_encode($productsData); - } - - /** - * Serialize a single product DB row to the full headless JSON structure. - * - * Includes every field declared on the Product domain model - * (Evomedien\Vitec\Domain\Model\Product) plus all fully resolved - * relations (categories, productimage, downloads, ogimage, relatedprodukt). - * - * DB-only columns that are NOT part of the domain model - * (cta, links, sorting1-5, key1-3, apptext1-3, productlayout, image, - * relatedimage, system fields) are intentionally omitted. - * - * @param array $product Associative DB row of tx_vitec_domain_model_product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - // --- identifier --- - 'uid' => $uid, - - // --- scalar string fields (Product domain model) --- - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - // --- boolean flags (Product domain model) --- - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - // --- convenience link (kept for backward compatibility) --- - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - // --- fully resolved relations --- - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - /** - * Get product images from FAL (sys_file_reference) - * Processes images through ImageService and generates srcset for responsive images - */ - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('sfr.uid', 'sfr.uid_local', '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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->executeQuery() - ->fetchAllAssociative(); - - $imageService = GeneralUtility::makeInstance(ImageService::class); - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $images = []; - foreach ($fileReferences as $fileRefData) { - try { - // Get FAL FileReference object - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - // Define image sizes for srcset - $sizes = [ - 'small' => ['width' => 400, 'height' => null], - 'medium' => ['width' => 800, 'height' => null], - 'large' => ['width' => 1200, 'height' => null], - 'xlarge' => ['width' => 1600, 'height' => null], - ]; - - $srcset = []; - foreach ($sizes as $sizeName => $dimensions) { - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - [ - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'crop' => $fileRefData['crop'] ?? null - ] - ); - - $imageUri = $imageService->getImageUri($processedImage); - $srcset[] = [ - 'url' => $imageUri, - 'width' => $dimensions['width'], - 'descriptor' => $dimensions['width'] . 'w' - ]; - } - - // Get original/default image - $defaultProcessed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - $images[] = [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($defaultProcessed), - '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) { - // Skip images that can't be processed - continue; - } - } - - return $images; - } - - /** - * Get the single Open Graph image (ogimage) for a product, or null. - * - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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']); - - $processed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($processed), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - /** - * Get categories for a product (resolved sys_category records). - */ - protected function getProductCategories(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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); - } - - /** - * Get all downloads for a product (resolved via tx_vitec_product_download_mm). - */ - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - // Get file information from FAL if file reference exists - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - /** - * Get related products (resolved via tx_vitec_product_related_mm). - * - * Returns a shallow representation (no nested relations) to avoid - * infinite recursion between mutually related products. - */ - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $images = $this->getProductImages($relUid); - - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $images, - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529111828 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529111828 deleted file mode 100755 index fd48d4e..0000000 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529111828 +++ /dev/null @@ -1,463 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - // Not a product-list page: emit nothing so headless removes the key. - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-list plugin element). - * Exception-safe: returns '' on any failure. - * - * @param array $contentElement tt_content row of the plugin - */ - public function renderForRecord(array $contentElement): string - { - try { - // Parse FlexForm of THIS element - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - $categoryUids = array_filter( - array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) - ); - $debugMode = (bool)($settings['debug'] ?? false); - $allProducts = (bool)($settings['allproducts'] ?? false); - - // Extbase repositories don't work in UserFunc context — direct query. - $productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productQuery = $productQueryBuilder - ->select('p.*') - ->from('tx_vitec_domain_model_product', 'p') - ->where( - $productQueryBuilder->expr()->eq('p.deleted', 0), - $productQueryBuilder->expr()->eq('p.hidden', 0), - $productQueryBuilder->expr()->eq('p.legacy', 0), - $productQueryBuilder->expr()->eq('p.supportproduct', 0), - $productQueryBuilder->expr()->eq('p.hideonwebsite', 0), - $productQueryBuilder->expr()->eq('p.hideonproducts', 0), - $productQueryBuilder->expr()->eq('p.subproduct', 0) - ); - - if (!empty($categoryUids) && !$allProducts) { - $productQuery - ->join( - 'p', - 'sys_category_record_mm', - 'mm', - 'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->andWhere( - $productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)) - ) - ->groupBy('p.uid'); - } - - $products = $productQuery->executeQuery()->fetchAllAssociative(); - - $productsData = []; - foreach ($products as $product) { - $productsData[] = $this->serializeProduct($product); - } - - if ($debugMode) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings, - ], - ]); - } - - return json_encode($productsData); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * Serialize a single product DB row to the full headless JSON structure. - * - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('sfr.uid', 'sfr.uid_local', '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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->executeQuery() - ->fetchAllAssociative(); - - $imageService = GeneralUtility::makeInstance(ImageService::class); - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $images = []; - foreach ($fileReferences as $fileRefData) { - try { - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - $sizes = [ - 'small' => ['width' => 400, 'height' => null], - 'medium' => ['width' => 800, 'height' => null], - 'large' => ['width' => 1200, 'height' => null], - 'xlarge' => ['width' => 1600, 'height' => null], - ]; - - $srcset = []; - foreach ($sizes as $sizeName => $dimensions) { - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - [ - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'crop' => $fileRefData['crop'] ?? null - ] - ); - - $imageUri = $imageService->getImageUri($processedImage); - $srcset[] = [ - 'url' => $imageUri, - 'width' => $dimensions['width'], - 'descriptor' => $dimensions['width'] . 'w' - ]; - } - - $defaultProcessed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - $images[] = [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($defaultProcessed), - '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) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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']); - - $processed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($processed), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - protected function getProductCategories(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529115809 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529115809 deleted file mode 100755 index 8aa4f9c..0000000 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529115809 +++ /dev/null @@ -1,507 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - // Not a product-list page: emit nothing so headless removes the key. - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-list plugin element). - * Exception-safe: returns '' on any failure. - * - * @param array $contentElement tt_content row of the plugin - */ - public function renderForRecord(array $contentElement): string - { - try { - // Parse FlexForm of THIS element - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - $categoryUids = array_filter( - array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) - ); - $debugMode = (bool)($settings['debug'] ?? false); - $allProducts = (bool)($settings['allproducts'] ?? false); - - // Extbase repositories don't work in UserFunc context — direct query. - $productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productQuery = $productQueryBuilder - ->select('p.*') - ->from('tx_vitec_domain_model_product', 'p') - ->where( - $productQueryBuilder->expr()->eq('p.deleted', 0), - $productQueryBuilder->expr()->eq('p.hidden', 0), - $productQueryBuilder->expr()->eq('p.legacy', 0), - $productQueryBuilder->expr()->eq('p.supportproduct', 0), - $productQueryBuilder->expr()->eq('p.hideonwebsite', 0), - $productQueryBuilder->expr()->eq('p.hideonproducts', 0), - $productQueryBuilder->expr()->eq('p.subproduct', 0) - ); - - if (!empty($categoryUids) && !$allProducts) { - $productQuery - ->join( - 'p', - 'sys_category_record_mm', - 'mm', - 'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->andWhere( - $productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)) - ) - ->groupBy('p.uid'); - } - - $products = $productQuery->executeQuery()->fetchAllAssociative(); - - $productsData = []; - foreach ($products as $product) { - $productsData[] = $this->serializeProduct($product); - } - - if ($debugMode) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings, - ], - ]); - } - - return json_encode($productsData); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * Serialize a single product DB row to the full headless JSON structure. - * - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'videofile' => $this->getProductVideoFile($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('sfr.uid', 'sfr.uid_local', '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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->executeQuery() - ->fetchAllAssociative(); - - $imageService = GeneralUtility::makeInstance(ImageService::class); - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $images = []; - foreach ($fileReferences as $fileRefData) { - try { - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - $sizes = [ - 'small' => ['width' => 400, 'height' => null], - 'medium' => ['width' => 800, 'height' => null], - 'large' => ['width' => 1200, 'height' => null], - 'xlarge' => ['width' => 1600, 'height' => null], - ]; - - $srcset = []; - foreach ($sizes as $sizeName => $dimensions) { - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - [ - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'crop' => $fileRefData['crop'] ?? null - ] - ); - - $imageUri = $imageService->getImageUri($processedImage); - $srcset[] = [ - 'url' => $imageUri, - 'width' => $dimensions['width'], - 'descriptor' => $dimensions['width'] . 'w' - ]; - } - - $defaultProcessed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - $images[] = [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($defaultProcessed), - '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) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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']); - - $processed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($processed), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - - /** - * Resolve the uploaded video file (single FAL reference, fieldname=videofile). - * - * @return array|null - */ - protected function getProductVideoFile(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $row = $queryBuilder - ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->from('sys_file_reference', 'fr') - ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') - ->where( - $queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), - $queryBuilder->expr()->eq('fr.deleted', 0), - $queryBuilder->expr()->eq('fr.hidden', 0), - $queryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$row) { - return null; - } - - return [ - 'uid' => (int)$row['file_uid'], - 'name' => (string)($row['name'] ?? ''), - 'url' => '/fileadmin' . ($row['identifier'] ?? ''), - 'size' => (int)($row['size'] ?? 0), - 'extension' => (string)($row['extension'] ?? ''), - 'mimeType' => (string)($row['mime_type'] ?? ''), - 'title' => (string)($row['title'] ?? ''), - 'description' => (string)($row['description'] ?? ''), - ]; - } - protected function getProductCategories(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529120751 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529120751 deleted file mode 100755 index fd1915c..0000000 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260529120751 +++ /dev/null @@ -1,507 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - // Not a product-list page: emit nothing so headless removes the key. - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-list plugin element). - * Exception-safe: returns '' on any failure. - * - * @param array $contentElement tt_content row of the plugin - */ - public function renderForRecord(array $contentElement): string - { - try { - // Parse FlexForm of THIS element - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - $categoryUids = array_filter( - array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) - ); - $debugMode = (bool)($settings['debug'] ?? false); - $allProducts = (bool)($settings['allproducts'] ?? false); - - // Extbase repositories don't work in UserFunc context — direct query. - $productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productQuery = $productQueryBuilder - ->select('p.*') - ->from('tx_vitec_domain_model_product', 'p') - ->where( - $productQueryBuilder->expr()->eq('p.deleted', 0), - $productQueryBuilder->expr()->eq('p.hidden', 0), - $productQueryBuilder->expr()->eq('p.legacy', 0), - $productQueryBuilder->expr()->eq('p.supportproduct', 0), - $productQueryBuilder->expr()->eq('p.hideonwebsite', 0), - $productQueryBuilder->expr()->eq('p.hideonproducts', 0), - $productQueryBuilder->expr()->eq('p.subproduct', 0) - ); - - if (!empty($categoryUids) && !$allProducts) { - $productQuery - ->join( - 'p', - 'sys_category_record_mm', - 'mm', - 'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->andWhere( - $productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)) - ) - ->groupBy('p.uid'); - } - - $products = $productQuery->executeQuery()->fetchAllAssociative(); - - $productsData = []; - foreach ($products as $product) { - $productsData[] = $this->serializeProduct($product); - } - - if ($debugMode) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings, - ], - ]); - } - - return json_encode($productsData); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * Serialize a single product DB row to the full headless JSON structure. - * - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')), - 'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelementcta'] ?? '')), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'videofile' => $this->getProductVideoFile($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('sfr.uid', 'sfr.uid_local', '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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('sfr.deleted', 0), - $queryBuilder->expr()->eq('sfr.hidden', 0) - ) - ->orderBy('sfr.sorting_foreign') - ->executeQuery() - ->fetchAllAssociative(); - - $imageService = GeneralUtility::makeInstance(ImageService::class); - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $images = []; - foreach ($fileReferences as $fileRefData) { - try { - $fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']); - - $sizes = [ - 'small' => ['width' => 400, 'height' => null], - 'medium' => ['width' => 800, 'height' => null], - 'large' => ['width' => 1200, 'height' => null], - 'xlarge' => ['width' => 1600, 'height' => null], - ]; - - $srcset = []; - foreach ($sizes as $sizeName => $dimensions) { - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - [ - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'crop' => $fileRefData['crop'] ?? null - ] - ); - - $imageUri = $imageService->getImageUri($processedImage); - $srcset[] = [ - 'url' => $imageUri, - 'width' => $dimensions['width'], - 'descriptor' => $dimensions['width'] . 'w' - ]; - } - - $defaultProcessed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] - ); - - $images[] = [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($defaultProcessed), - '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) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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']); - - $processed = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200, 'crop' => $fileRefData['crop'] ?? null] - ); - - return [ - 'uid' => (int)$fileRefData['uid'], - 'url' => $imageService->getImageUri($processed), - 'title' => $fileRefData['title'] ?? '', - 'alternative' => $fileRefData['alternative'] ?? '', - 'description' => $fileRefData['description'] ?? '', - 'properties' => [ - 'width' => $fileReference->getProperty('width'), - 'height' => $fileReference->getProperty('height'), - 'mimeType' => $fileReference->getProperty('mime_type'), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - - /** - * Resolve the uploaded video file (single FAL reference, fieldname=videofile). - * - * @return array|null - */ - protected function getProductVideoFile(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $row = $queryBuilder - ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->from('sys_file_reference', 'fr') - ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') - ->where( - $queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), - $queryBuilder->expr()->eq('fr.deleted', 0), - $queryBuilder->expr()->eq('fr.hidden', 0), - $queryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$row) { - return null; - } - - return [ - 'uid' => (int)$row['file_uid'], - 'name' => (string)($row['name'] ?? ''), - 'url' => '/fileadmin' . ($row['identifier'] ?? ''), - 'size' => (int)($row['size'] ?? 0), - 'extension' => (string)($row['extension'] ?? ''), - 'mimeType' => (string)($row['mime_type'] ?? ''), - 'title' => (string)($row['title'] ?? ''), - 'description' => (string)($row['description'] ?? ''), - ]; - } - protected function getProductCategories(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, 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); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php old mode 100755 new mode 100644 index 0ffcb89..78b84a3 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; + +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -23,11 +25,32 @@ use Doctrine\DBAL\ParameterType; */ class ProductShowJsonRenderer { + #[AsAllowedCallable] public function render(string $content, array $conf): string { - $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + // 1) cObj data path + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_productshow') { + return $this->renderForRecord($row); + } - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + // 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) { + $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 ''; + } + + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); $contentElements = $queryBuilder @@ -35,7 +58,7 @@ class ProductShowJsonRenderer ->from('tt_content') ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), $queryBuilder->expr()->eq('deleted', 0), $queryBuilder->expr()->eq('hidden', 0) ) @@ -49,6 +72,7 @@ class ProductShowJsonRenderer return $this->renderForRecord($contentElements[0]); } + /** * Render exactly the given tt_content row (the product-show plugin element). * Exception-safe: returns '' on any failure. diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 deleted file mode 100755 index 8bb69b3..0000000 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 +++ /dev/null @@ -1,366 +0,0 @@ -id; - - // DEBUG: Log that the UserFunc is being called - $debugInfo = [ - 'userFuncCalled' => true, - 'pageId' => $pageId, - 'requestUri' => $_SERVER['REQUEST_URI'] ?? 'unknown', - ]; - - // Query tt_content for vitec_productshow on this page - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - $debugInfo['contentElementsFound'] = count($contentElements); - - if (empty($contentElements)) { - $debugInfo['error'] = 'No vitec_productshow on page'; - return json_encode(['debug' => $debugInfo]); - } - - // Take the first one - $contentElement = $contentElements[0]; - - // Parse FlexForm - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - // Get product UID from FlexForm or route parameter - $productUid = (int)($settings['product'] ?? 0); - $layout = (int)($settings['layout'] ?? 0); - $debugMode = (bool)($settings['debug'] ?? false); - - // If no product selected in FlexForm, try to get from route parameter - if (!$productUid) { - // Get the product parameter from GET request - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $productParam = $routeParams['tx_vitec_productshow']['product'] ?? null; - - if ($productParam) { - // If it's a slug, resolve it to UID - if (!is_numeric($productParam)) { - $slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productBySlug = $slugQueryBuilder - ->select('uid') - ->from('tx_vitec_domain_model_product') - ->where( - $slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)), - $slugQueryBuilder->expr()->eq('deleted', 0), - $slugQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - $productUid = (int)($productBySlug['uid'] ?? 0); - } else { - $productUid = (int)$productParam; - } - } - } - - if (!$productUid) { - return json_encode([ - 'error' => 'No product selected or found', - 'debug' => [ - 'settings' => $settings, - 'routeParams' => $routeParams ?? [], - 'allQueryParams' => $GLOBALS['TYPO3_REQUEST']->getQueryParams() ?? [], - 'requestUri' => $GLOBALS['TYPO3_REQUEST']->getUri()->getPath() ?? '' - ] - ]); - } - - // Query product - $productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $product = $productQueryBuilder - ->select('*') - ->from('tx_vitec_domain_model_product') - ->where( - $productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $productQueryBuilder->expr()->eq('deleted', 0), - $productQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - if (!$product) { - return json_encode([ - 'error' => 'Product not found', - 'debug' => $debugMode ? ['productUid' => $productUid] : null - ]); - } - - // Get product images - $images = $this->getProductImages((int)$product['uid']); - - // Get categories - $categories = $this->getProductCategories((int)$product['uid']); - - // Get downloads - $downloads = $this->getProductDownloads((int)$product['uid']); - - // Build response - $response = [ - 'product' => [ - 'uid' => (int)$product['uid'], - 'title' => $product['title'], - 'subtitle' => $product['subtitle'], - 'slug' => $product['slug'], - 'teaser' => $product['teaser'], - 'description' => $product['description'], - 'seotitle' => $product['seotitle'], - 'categories' => $categories, - 'images' => $images, - 'downloads' => $downloads, - ], - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, - ]; - } - - return json_encode($response); - } - - /** - * Get all images for a product with FAL and ImageService processing - */ - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($fileReferences)) { - return []; - } - - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $images = []; - - foreach ($fileReferences as $fileRef) { - try { - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - // Process main image - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => '1874c', 'height' => '625c'] - ); - - // Generate srcset - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width . 'c', 'height' => (int)($width / 3) . 'c'] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $images[] = [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - // Skip invalid file references - continue; - } - } - - return $images; - } - - /** - * Get categories for a product - */ - protected function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'], - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } - - /** - * Get all downloads for a product - */ - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - // Get file information from FAL if file reference exists - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 deleted file mode 100755 index dda264f..0000000 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 +++ /dev/null @@ -1,512 +0,0 @@ -id; - - // DEBUG: Log that the UserFunc is being called - $debugInfo = [ - 'userFuncCalled' => true, - 'pageId' => $pageId, - 'requestUri' => $_SERVER['REQUEST_URI'] ?? 'unknown', - ]; - - // Query tt_content for vitec_productshow on this page - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - $debugInfo['contentElementsFound'] = count($contentElements); - - if (empty($contentElements)) { - $debugInfo['error'] = 'No vitec_productshow on page'; - return json_encode(['debug' => $debugInfo]); - } - - // Take the first one - $contentElement = $contentElements[0]; - - // Parse FlexForm - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - - // Get product UID from FlexForm or route parameter - $productUid = (int)($settings['product'] ?? 0); - $layout = (int)($settings['layout'] ?? 0); - $debugMode = (bool)($settings['debug'] ?? false); - - // If no product selected in FlexForm, try to get from route parameter - if (!$productUid) { - // Get the product parameter from GET request - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $productParam = $routeParams['tx_vitec_productshow']['product'] ?? null; - - if ($productParam) { - // If it's a slug, resolve it to UID - if (!is_numeric($productParam)) { - $slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productBySlug = $slugQueryBuilder - ->select('uid') - ->from('tx_vitec_domain_model_product') - ->where( - $slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)), - $slugQueryBuilder->expr()->eq('deleted', 0), - $slugQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - $productUid = (int)($productBySlug['uid'] ?? 0); - } else { - $productUid = (int)$productParam; - } - } - } - - if (!$productUid) { - return json_encode([ - 'error' => 'No product selected or found', - 'debug' => [ - 'settings' => $settings, - 'routeParams' => $routeParams ?? [], - 'allQueryParams' => $GLOBALS['TYPO3_REQUEST']->getQueryParams() ?? [], - 'requestUri' => $GLOBALS['TYPO3_REQUEST']->getUri()->getPath() ?? '' - ] - ]); - } - - // Query product - $productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $product = $productQueryBuilder - ->select('*') - ->from('tx_vitec_domain_model_product') - ->where( - $productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $productQueryBuilder->expr()->eq('deleted', 0), - $productQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - if (!$product) { - return json_encode([ - 'error' => 'Product not found', - 'debug' => $debugMode ? ['productUid' => $productUid] : null - ]); - } - - // Build response (full Product domain model + resolved relations) - $response = [ - 'product' => $this->serializeProduct($product), - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, - ]; - } - - return json_encode($response); - } - - /** - * Serialize a single product DB row to the full headless JSON structure. - * - * Includes every field declared on the Product domain model - * (Evomedien\Vitec\Domain\Model\Product) plus all fully resolved - * relations (categories, productimage, downloads, ogimage, relatedprodukt). - * - * Kept structurally identical to ProductListJsonRenderer::serializeProduct() - * so list and detail endpoints expose the same product schema. - * - * @param array $product Associative DB row of tx_vitec_domain_model_product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - // --- identifier --- - 'uid' => $uid, - - // --- scalar string fields (Product domain model) --- - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - // --- boolean flags (Product domain model) --- - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - // --- convenience link (kept for backward compatibility) --- - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - // --- fully resolved relations --- - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - /** - * Get all images for a product with FAL and ImageService processing - */ - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($fileReferences)) { - return []; - } - - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $images = []; - - foreach ($fileReferences as $fileRef) { - try { - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - // Process main image - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => '1874c', 'height' => '625c'] - ); - - // Generate srcset - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width . 'c', 'height' => (int)($width / 3) . 'c'] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $images[] = [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - // Skip invalid file references - continue; - } - } - - return $images; - } - - /** - * Get the single Open Graph image (ogimage) for a product, or null. - * - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$fileRef) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200] - ); - - return [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - /** - * Get categories for a product - */ - protected function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'], - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } - - /** - * Get all downloads for a product - */ - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - // Get file information from FAL if file reference exists - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - /** - * Get related products (resolved via tx_vitec_product_related_mm). - * - * Returns a shallow representation (no nested relations) to avoid - * infinite recursion between mutually related products. - */ - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529111828 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529111828 deleted file mode 100755 index a7ae85b..0000000 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529111828 +++ /dev/null @@ -1,470 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-show plugin element). - * Exception-safe: returns '' on any failure. - * - * @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'] ?? []; - - $productUid = (int)($settings['product'] ?? 0); - $layout = (int)($settings['layout'] ?? 0); - $debugMode = (bool)($settings['debug'] ?? false); - - if (!$productUid) { - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $productParam = $routeParams['tx_vitec_productshow']['product'] ?? null; - - if ($productParam) { - if (!is_numeric($productParam)) { - $slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productBySlug = $slugQueryBuilder - ->select('uid') - ->from('tx_vitec_domain_model_product') - ->where( - $slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)), - $slugQueryBuilder->expr()->eq('deleted', 0), - $slugQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - $productUid = (int)($productBySlug['uid'] ?? 0); - } else { - $productUid = (int)$productParam; - } - } - } - - if (!$productUid) { - return $debugMode - ? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]]) - : ''; - } - - $productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $product = $productQueryBuilder - ->select('*') - ->from('tx_vitec_domain_model_product') - ->where( - $productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $productQueryBuilder->expr()->eq('deleted', 0), - $productQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - if (!$product) { - return $debugMode - ? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]]) - : ''; - } - - $response = [ - 'product' => $this->serializeProduct($product), - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, - ]; - } - - return json_encode($response); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($fileReferences)) { - return []; - } - - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $images = []; - - foreach ($fileReferences as $fileRef) { - try { - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => '1874c', 'height' => '625c'] - ); - - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width . 'c', 'height' => (int)($width / 3) . 'c'] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $images[] = [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$fileRef) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200] - ); - - return [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - protected function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'], - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529115809 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529115809 deleted file mode 100755 index ef54bcf..0000000 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529115809 +++ /dev/null @@ -1,514 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-show plugin element). - * Exception-safe: returns '' on any failure. - * - * @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'] ?? []; - - $productUid = (int)($settings['product'] ?? 0); - $layout = (int)($settings['layout'] ?? 0); - $debugMode = (bool)($settings['debug'] ?? false); - - if (!$productUid) { - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $productParam = $routeParams['tx_vitec_productshow']['product'] ?? null; - - if ($productParam) { - if (!is_numeric($productParam)) { - $slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productBySlug = $slugQueryBuilder - ->select('uid') - ->from('tx_vitec_domain_model_product') - ->where( - $slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)), - $slugQueryBuilder->expr()->eq('deleted', 0), - $slugQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - $productUid = (int)($productBySlug['uid'] ?? 0); - } else { - $productUid = (int)$productParam; - } - } - } - - if (!$productUid) { - return $debugMode - ? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]]) - : ''; - } - - $productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $product = $productQueryBuilder - ->select('*') - ->from('tx_vitec_domain_model_product') - ->where( - $productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $productQueryBuilder->expr()->eq('deleted', 0), - $productQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - if (!$product) { - return $debugMode - ? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]]) - : ''; - } - - $response = [ - 'product' => $this->serializeProduct($product), - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, - ]; - } - - return json_encode($response); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => (string)($product['contentelement'] ?? ''), - 'contentelementcta' => (string)($product['contentelementcta'] ?? ''), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'videofile' => $this->getProductVideoFile($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($fileReferences)) { - return []; - } - - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $images = []; - - foreach ($fileReferences as $fileRef) { - try { - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => '1874c', 'height' => '625c'] - ); - - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width . 'c', 'height' => (int)($width / 3) . 'c'] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $images[] = [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$fileRef) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200] - ); - - return [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - - /** - * Resolve the uploaded video file (single FAL reference, fieldname=videofile). - * - * @return array|null - */ - protected function getProductVideoFile(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $row = $queryBuilder - ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->from('sys_file_reference', 'fr') - ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') - ->where( - $queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), - $queryBuilder->expr()->eq('fr.deleted', 0), - $queryBuilder->expr()->eq('fr.hidden', 0), - $queryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$row) { - return null; - } - - return [ - 'uid' => (int)$row['file_uid'], - 'name' => (string)($row['name'] ?? ''), - 'url' => '/fileadmin' . ($row['identifier'] ?? ''), - 'size' => (int)($row['size'] ?? 0), - 'extension' => (string)($row['extension'] ?? ''), - 'mimeType' => (string)($row['mime_type'] ?? ''), - 'title' => (string)($row['title'] ?? ''), - 'description' => (string)($row['description'] ?? ''), - ]; - } - protected function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'], - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529120751 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529120751 deleted file mode 100755 index 5b86e32..0000000 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260529120751 +++ /dev/null @@ -1,514 +0,0 @@ -id ?? 0); - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return ''; - } - - return $this->renderForRecord($contentElements[0]); - } - - /** - * Render exactly the given tt_content row (the product-show plugin element). - * Exception-safe: returns '' on any failure. - * - * @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'] ?? []; - - $productUid = (int)($settings['product'] ?? 0); - $layout = (int)($settings['layout'] ?? 0); - $debugMode = (bool)($settings['debug'] ?? false); - - if (!$productUid) { - $routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams(); - $productParam = $routeParams['tx_vitec_productshow']['product'] ?? null; - - if ($productParam) { - if (!is_numeric($productParam)) { - $slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $productBySlug = $slugQueryBuilder - ->select('uid') - ->from('tx_vitec_domain_model_product') - ->where( - $slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)), - $slugQueryBuilder->expr()->eq('deleted', 0), - $slugQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - $productUid = (int)($productBySlug['uid'] ?? 0); - } else { - $productUid = (int)$productParam; - } - } - } - - if (!$productUid) { - return $debugMode - ? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]]) - : ''; - } - - $productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $product = $productQueryBuilder - ->select('*') - ->from('tx_vitec_domain_model_product') - ->where( - $productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $productQueryBuilder->expr()->eq('deleted', 0), - $productQueryBuilder->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAssociative(); - - if (!$product) { - return $debugMode - ? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]]) - : ''; - } - - $response = [ - 'product' => $this->serializeProduct($product), - 'layout' => $layout, - 'settings' => [ - 'layout' => $layout, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, - ]; - } - - return json_encode($response); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * @param array $product - * @return array - */ - protected function serializeProduct(array $product): array - { - $uid = (int)$product['uid']; - - return [ - 'uid' => $uid, - - 'title' => (string)($product['title'] ?? ''), - 'slug' => (string)($product['slug'] ?? ''), - 'urltitle' => (string)($product['urltitle'] ?? ''), - 'seotitle' => (string)($product['seotitle'] ?? ''), - 'seometa' => (string)($product['seometa'] ?? ''), - 'keywords' => (string)($product['keywords'] ?? ''), - 'structureddata' => (string)($product['structureddata'] ?? ''), - 'teaser' => (string)($product['teaser'] ?? ''), - 'subtitle' => (string)($product['subtitle'] ?? ''), - 'video' => (string)($product['video'] ?? ''), - 'applications' => (string)($product['applications'] ?? ''), - 'description' => (string)($product['description'] ?? ''), - 'highlights' => (string)($product['highlights'] ?? ''), - 'shortcutpid' => (string)($product['shortcutpid'] ?? ''), - 'contentelement' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')), - 'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelementcta'] ?? '')), - - 'hideonapp' => (bool)($product['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false), - 'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false), - 'hideonproducts' => (bool)($product['hideonproducts'] ?? false), - 'shortcut' => (bool)($product['shortcut'] ?? false), - 'legacy' => (bool)($product['legacy'] ?? false), - 'supportproduct' => (bool)($product['supportproduct'] ?? false), - 'subproduct' => (bool)($product['subproduct'] ?? false), - - 'link' => '/product/' . (string)($product['slug'] ?? ''), - - 'categories' => $this->getProductCategories($uid), - 'images' => $this->getProductImages($uid), - 'downloads' => $this->getProductDownloads($uid), - 'ogimage' => $this->getProductOgImage($uid), - 'videofile' => $this->getProductVideoFile($uid), - 'relatedprodukt' => $this->getRelatedProducts($uid), - ]; - } - - protected function getProductImages(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileReferences = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($fileReferences)) { - return []; - } - - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $images = []; - - foreach ($fileReferences as $fileRef) { - try { - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => '1874c', 'height' => '625c'] - ); - - $srcset = []; - foreach ([400, 800, 1200, 1600] as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width . 'c', 'height' => (int)($width / 3) . 'c'] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - 'width' => $width, - 'descriptor' => $width . 'w', - ]; - } - - $images[] = [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'srcset' => $srcset, - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - continue; - } - } - - return $images; - } - - /** - * @return array|null - */ - protected function getProductOgImage(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $queryBuilder - ->select('*') - ->from('sys_file_reference') - ->where( - $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), - $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)), - $queryBuilder->expr()->eq('deleted', 0), - $queryBuilder->expr()->eq('hidden', 0) - ) - ->orderBy('sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$fileRef) { - return null; - } - - try { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $imageService = GeneralUtility::makeInstance(ImageService::class); - $fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']); - $originalFile = $fileReference->getOriginalFile(); - - $processedImage = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => 1200] - ); - - return [ - 'uid' => $fileRef['uid'], - 'url' => $imageService->getImageUri($processedImage), - 'title' => $fileReference->getTitle() ?: '', - 'alternative' => $fileReference->getAlternative() ?: '', - 'description' => $fileReference->getDescription() ?: '', - 'properties' => [ - 'width' => $originalFile->getProperty('width'), - 'height' => $originalFile->getProperty('height'), - 'mimeType' => $originalFile->getMimeType(), - ], - ]; - } catch (\Exception $e) { - return null; - } - } - - - /** - * Resolve the uploaded video file (single FAL reference, fieldname=videofile). - * - * @return array|null - */ - protected function getProductVideoFile(int $productUid): ?array - { - $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $row = $queryBuilder - ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->from('sys_file_reference', 'fr') - ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') - ->where( - $queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)), - $queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), - $queryBuilder->expr()->eq('fr.deleted', 0), - $queryBuilder->expr()->eq('fr.hidden', 0), - $queryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if (!$row) { - return null; - } - - return [ - 'uid' => (int)$row['file_uid'], - 'name' => (string)($row['name'] ?? ''), - 'url' => '/fileadmin' . ($row['identifier'] ?? ''), - 'size' => (int)($row['size'] ?? 0), - 'extension' => (string)($row['extension'] ?? ''), - 'mimeType' => (string)($row['mime_type'] ?? ''), - 'title' => (string)($row['title'] ?? ''), - 'description' => (string)($row['description'] ?? ''), - ]; - } - protected function getProductCategories(int $productUid): 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_product', ParameterType::STRING) . - ' AND mm.fieldname = ' . - $queryBuilder->createNamedParameter('categories', ParameterType::STRING) - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('c.deleted', 0), - $queryBuilder->expr()->eq('c.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - return array_map(function ($cat) { - return [ - 'uid' => (int)$cat['uid'], - 'title' => $cat['title'], - 'description' => $cat['description'] ?? '', - ]; - }, $categories); - } - - protected function getProductDownloads(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_download'); - - $downloads = $queryBuilder - ->select('d.*') - ->from('tx_vitec_domain_model_download', 'd') - ->join( - 'd', - 'tx_vitec_product_download_mm', - 'mm', - 'mm.uid_foreign = d.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('d.deleted', 0), - $queryBuilder->expr()->eq('d.hidden', 0), - $queryBuilder->expr()->eq('d.hideonwebsite', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($downloads as $download) { - $fileInfo = null; - - if (!empty($download['file'])) { - $fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_file_reference'); - - $fileRef = $fileQueryBuilder - ->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') - ->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.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), - $fileQueryBuilder->expr()->eq('f.missing', 0) - ) - ->orderBy('fr.sorting_foreign', 'ASC') - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - - if ($fileRef) { - $fileInfo = [ - 'uid' => (int)$fileRef['file_uid'], - 'name' => $fileRef['name'], - 'url' => '/fileadmin' . $fileRef['identifier'], - 'size' => (int)$fileRef['size'], - 'extension' => $fileRef['extension'], - 'mimeType' => $fileRef['mime_type'] ?? '', - ]; - } - } - - $result[] = [ - 'uid' => (int)$download['uid'], - 'title' => $download['title'] ?? '', - 'slug' => $download['slug'] ?? '', - 'teaser' => $download['teaser'] ?? '', - 'description' => $download['description'] ?? '', - 'keywords' => $download['keywords'] ?? '', - 'icon' => $download['icon'] ?? '', - 'file' => $fileInfo, - ]; - } - - return $result; - } - - protected function getRelatedProducts(int $productUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_product'); - - $related = $queryBuilder - ->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description') - ->from('tx_vitec_domain_model_product', 'p') - ->join( - 'p', - 'tx_vitec_product_related_mm', - 'mm', - 'mm.uid_foreign = p.uid' - ) - ->where( - $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), - $queryBuilder->expr()->eq('p.deleted', 0), - $queryBuilder->expr()->eq('p.hidden', 0) - ) - ->orderBy('mm.sorting', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $result = []; - foreach ($related as $rel) { - $relUid = (int)$rel['uid']; - $result[] = [ - 'uid' => $relUid, - 'title' => (string)($rel['title'] ?? ''), - 'slug' => (string)($rel['slug'] ?? ''), - 'subtitle' => (string)($rel['subtitle'] ?? ''), - 'teaser' => (string)($rel['teaser'] ?? ''), - 'description' => (string)($rel['description'] ?? ''), - 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $this->getProductImages($relUid), - ]; - } - - return $result; - } -} diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php old mode 100755 new mode 100644 similarity index 54% rename from packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 rename to packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php index e469c96..aa77f4d --- a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 +++ b/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; + +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -12,106 +14,115 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Service\ImageService; /** - * UserFunc to render a single usecase as JSON for headless output. + * UserFunc to render a single solution as JSON for headless output. * - * Mirrors ProductShowJsonRenderer. Extbase repositories are unavailable in a - * UserFunc context, so tt_content and the usecase record are queried directly. - * Exception-safe: returns '' on any failure / when not applicable so headless - * `ifEmptyUnsetKey` drops the key entirely. + * `render()` = page discovery (top-level plugin). `renderForRecord()` = + * one specific tt_content row, reused by ContainerChildrenProcessor and + * ContentElementResolver. Exception-safe. */ -class UsecaseShowJsonRenderer +class SolutionShowJsonRenderer { + #[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_solutionshow') { + 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) { + $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 ''; + } + + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tt_content'); + + $contentElements = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)), + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_solutionshow', ParameterType::STRING)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->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 { $pageId = (int)($GLOBALS['TSFE']->id ?? 0); - // Find the vitec_usecaseshow plugin on this page - $ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $ttContentQb - ->select('*') - ->from('tt_content') - ->where( - $ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)), - $ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)), - $ttContentQb->expr()->eq('deleted', 0), - $ttContentQb->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - return ''; - } - - $contentElement = $contentElements[0]; - - // Parse FlexForm $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); $settings = $flexFormData['settings'] ?? []; - $usecaseUid = (int)($settings['usecase'] ?? 0); + $solutionUid = (int)($settings['solution'] ?? 0); $layout = (string)($settings['layout'] ?? 'default'); $debugMode = (bool)($settings['debug'] ?? false); - // Fall back to route / query parameter (uid or slug) - if (!$usecaseUid) { + // Fallback: route / query parameter (numeric uid only — Market has no slug) + if (!$solutionUid) { $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; - } + $solutionParam = $routeParams['tx_vitec_solutionshow']['solution'] ?? null; + if ($solutionParam && is_numeric($solutionParam)) { + $solutionUid = (int)$solutionParam; } } - if (!$usecaseUid) { + if (!$solutionUid) { return $debugMode - ? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]]) + ? json_encode(['error' => 'No solution selected or found', 'debug' => ['settings' => $settings]]) : ''; } - // Query the usecase - $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); + $solutionQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_solution'); - $usecase = $usecaseQb + $solution = $solutionQb ->select('*') - ->from('tx_vitec_domain_model_usecase') + ->from('tx_vitec_domain_model_solution') ->where( - $usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), - $usecaseQb->expr()->eq('deleted', 0), - $usecaseQb->expr()->eq('hidden', 0) + $solutionQb->expr()->eq('uid', $solutionQb->createNamedParameter($solutionUid, ParameterType::INTEGER)), + $solutionQb->expr()->eq('deleted', 0), + $solutionQb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAssociative(); - if (!$usecase) { + if (!$solution) { return $debugMode - ? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]]) + ? json_encode(['error' => 'Solution not found', 'debug' => ['solutionUid' => $solutionUid]]) : ''; } $response = [ - 'usecase' => $this->serializeUsecase($usecase), + 'solution' => $this->serializeSolution($solution), 'layout' => $layout, 'settings' => [ 'layout' => $layout, @@ -121,7 +132,7 @@ class UsecaseShowJsonRenderer if ($debugMode) { $response['debug'] = [ 'pageId' => $pageId, - 'usecaseUid' => $usecaseUid, + 'solutionUid' => $solutionUid, 'layout' => $layout, 'settings' => $settings, ]; @@ -134,39 +145,30 @@ class UsecaseShowJsonRenderer } /** - * Serialize a single usecase DB row. Structurally identical to - * UsecaseListJsonRenderer::serializeUsecase() so list and detail - * endpoints expose the same usecase schema. - * - * @param array $usecase + * @param array $solution * @return array */ - protected function serializeUsecase(array $usecase): array + protected function serializeSolution(array $solution): array { - $uid = (int)$usecase['uid']; + $uid = (int)$solution['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'), + 'title' => (string)($solution['title'] ?? ''), + 'subtitle' => (string)($solution['subtitle'] ?? ''), + 'teaser' => (string)($solution['teaser'] ?? ''), + 'description' => (string)($solution['description'] ?? ''), + 'categories' => $this->getSolutionCategories($uid), + 'image' => $this->getSolutionImage($uid), ]; } /** - * Resolve a single FAL image field (caseimage / logoimage) with srcset. + * Resolve the single image FAL reference for a market (fieldname=image). * * @return array|null */ - protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array + protected function getSolutionImage(int $solutionUid): ?array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); @@ -175,9 +177,9 @@ class UsecaseShowJsonRenderer ->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.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_solution', ParameterType::STRING)), + $queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('image', ParameterType::STRING)), + $queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($solutionUid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('sfr.deleted', 0), $queryBuilder->expr()->eq('sfr.hidden', 0) ) @@ -231,10 +233,7 @@ class UsecaseShowJsonRenderer } } - /** - * Get categories for a usecase (resolved sys_category records). - */ - protected function getUsecaseCategories(int $usecaseUid): array + protected function getSolutionCategories(int $solutionUid): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_category'); @@ -247,12 +246,12 @@ class UsecaseShowJsonRenderer 'sys_category_record_mm', 'mm', 'mm.uid_local = c.uid AND mm.tablenames = ' . - $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) . + $queryBuilder->createNamedParameter('tx_vitec_domain_model_solution', 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('mm.uid_foreign', $queryBuilder->createNamedParameter($solutionUid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('c.deleted', 0), $queryBuilder->expr()->eq('c.hidden', 0) ) diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php old mode 100755 new mode 100644 index b3792e0..d6e6852 --- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; + +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService; */ class UsecaseListJsonRenderer { + #[AsAllowedCallable] public function render(string $content, array $conf): string { - $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + // 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); + } - $ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class) + // 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) { + $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 ''; + } + + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); - $contentElements = $ttContentQb + $contentElements = $queryBuilder ->select('*') ->from('tt_content') ->where( - $ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)), - $ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)), - $ttContentQb->expr()->eq('deleted', 0), - $ttContentQb->expr()->eq('hidden', 0) + $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) ) ->executeQuery() ->fetchAllAssociative(); @@ -46,6 +69,7 @@ class UsecaseListJsonRenderer return $this->renderForRecord($contentElements[0]); } + /** * @param array $contentElement */ diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 deleted file mode 100755 index 91b3d8d..0000000 --- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 +++ /dev/null @@ -1,244 +0,0 @@ -id ?? 0); - - // Find the vitec_usecaselist plugin on this page - $ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - - $contentElements = $ttContentQb - ->select('*') - ->from('tt_content') - ->where( - $ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)), - $ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)), - $ttContentQb->expr()->eq('deleted', 0), - $ttContentQb->expr()->eq('hidden', 0) - ) - ->executeQuery() - ->fetchAllAssociative(); - - if (empty($contentElements)) { - // Not a usecase-list page (or none configured): emit nothing - // so headless removes the key entirely. - return ''; - } - - $contentElement = $contentElements[0]; - - // Parse FlexForm (debug flag only; the list shows all visible usecases) - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; - $debugMode = (bool)($settings['debug'] ?? false); - - // Query visible usecases directly (Extbase unavailable in UserFunc) - $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); - - $usecases = $usecaseQb - ->select('u.*') - ->from('tx_vitec_domain_model_usecase', 'u') - ->where( - $usecaseQb->expr()->eq('u.deleted', 0), - $usecaseQb->expr()->eq('u.hidden', 0), - $usecaseQb->expr()->eq('u.hideonwebsite', 0) - ) - ->orderBy('u.title', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $usecasesData = []; - foreach ($usecases as $usecase) { - $usecasesData[] = $this->serializeUsecase($usecase); - } - - if ($debugMode) { - return json_encode([ - 'usecases' => $usecasesData, - 'debug' => [ - 'pageId' => $pageId, - 'usecaseCount' => count($usecasesData), - 'settings' => $settings, - ], - ]); - } - - return json_encode($usecasesData); - } catch (\Throwable $e) { - return ''; - } - } - - /** - * Serialize a single usecase DB row to the full headless JSON structure. - * - * Includes every field declared on the Usecase domain model - * (Evomedien\Vitec\Domain\Model\Usecase) plus all resolved relations - * (categories, caseimage, logoimage). Kept structurally analogous to - * ProductListJsonRenderer::serializeProduct(). - * - * @param array $usecase - * @return array - */ - protected function serializeUsecase(array $usecase): array - { - $uid = (int)$usecase['uid']; - - return [ - // --- identifier --- - 'uid' => $uid, - - // --- scalar string fields (Usecase domain model) --- - 'title' => (string)($usecase['title'] ?? ''), - 'slug' => (string)($usecase['slug'] ?? ''), - 'subtitle' => (string)($usecase['subtitle'] ?? ''), - 'teaser' => (string)($usecase['teaser'] ?? ''), - 'description' => (string)($usecase['description'] ?? ''), - 'singlepid' => (string)($usecase['singlepid'] ?? ''), - - // --- boolean flags (Usecase domain model) --- - 'hideonapp' => (bool)($usecase['hideonapp'] ?? false), - 'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false), - - // --- fully resolved relations --- - 'categories' => $this->getUsecaseCategories($uid), - 'caseimage' => $this->getUsecaseImage($uid, 'caseimage'), - 'logoimage' => $this->getUsecaseImage($uid, 'logoimage'), - ]; - } - - /** - * Resolve a single FAL image field (caseimage / logoimage) with srcset. - * - * @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']); - - $sizes = [400, 800, 1200, 1600]; - $srcset = []; - foreach ($sizes as $width) { - $processedVariant = $imageService->applyProcessingInstructions( - $fileReference, - ['width' => $width, 'crop' => $fileRefData['crop'] ?? null] - ); - $srcset[] = [ - 'url' => $imageService->getImageUri($processedVariant), - '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; - } - } - - /** - * Get categories for a usecase (resolved sys_category records). - */ - 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 b57f391..49459ec --- a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Evomedien\Vitec\UserFunc; + +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use Doctrine\DBAL\ParameterType; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService; */ class UsecaseShowJsonRenderer { + #[AsAllowedCallable] public function render(string $content, array $conf): string { - $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + // 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); + } - $ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class) + // 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) { + $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 ''; + } + + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); - $contentElements = $ttContentQb + $contentElements = $queryBuilder ->select('*') ->from('tt_content') ->where( - $ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)), - $ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)), - $ttContentQb->expr()->eq('deleted', 0), - $ttContentQb->expr()->eq('hidden', 0) + $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) ) ->executeQuery() ->fetchAllAssociative(); @@ -46,6 +69,7 @@ class UsecaseShowJsonRenderer return $this->renderForRecord($contentElements[0]); } + /** * @param array $contentElement */ diff --git a/packages/vitec/Classes/View/BackendLayoutDataProvider.php b/packages/vitec/Classes/View/BackendLayoutDataProvider.php old mode 100755 new mode 100644 index 635ec4e..c21887d --- a/packages/vitec/Classes/View/BackendLayoutDataProvider.php +++ b/packages/vitec/Classes/View/BackendLayoutDataProvider.php @@ -62,6 +62,11 @@ final class BackendLayoutDataProvider implements DataProviderInterface 'preFooter' => 'Pre-Footer', ]; + public function getIdentifier(): string + { + return 'vitec'; + } + public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection) { foreach (self::LAYOUT_MAP as $value => $info) { diff --git a/packages/vitec/Classes/View/VitecBackendLayoutView.php b/packages/vitec/Classes/View/VitecBackendLayoutView.php old mode 100755 new mode 100644 index afe3479..d62b954 --- a/packages/vitec/Classes/View/VitecBackendLayoutView.php +++ b/packages/vitec/Classes/View/VitecBackendLayoutView.php @@ -10,7 +10,7 @@ use TYPO3\CMS\Backend\View\BackendLayoutView; * Overrides BackendLayoutView so the page module always reflects pages.layout * without the editor having to set backend_layout manually. */ -final class VitecBackendLayoutView extends BackendLayoutView +final readonly class VitecBackendLayoutView extends BackendLayoutView { public function getSelectedCombinedIdentifier(int $pageId): string|false { 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/Datasheets.xml b/packages/vitec/Configuration/FlexForms/Datasheets.xml index 03a4ac2..75cb4a9 100644 --- a/packages/vitec/Configuration/FlexForms/Datasheets.xml +++ b/packages/vitec/Configuration/FlexForms/Datasheets.xml @@ -6,60 +6,42 @@ - - General Settings - + General Settings array - - - - check - 1 - - + + + check + 1 + - + - - - - check - 1 - - + + + check + 1 + - + - - - - number - 20 - - + + + number + 20 + - - - - - - select - selectSingle - tx_vitec_domain_model_downloadcategory - ORDER BY title - - - - 0 - - - - - + + + + + check + 0 + + - \ No newline at end of file + diff --git a/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 b/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 deleted file mode 100755 index f7a1cb6..0000000 --- a/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - Select Usecase - - array - - - - - select - selectSingle - tx_vitec_domain_model_usecase - AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title - 1 - 0 - 1 - - - - - - - \ No newline at end of file diff --git a/packages/vitec/Configuration/Icons.php b/packages/vitec/Configuration/Icons.php old mode 100755 new mode 100644 index 02328b4..0925a62 --- a/packages/vitec/Configuration/Icons.php +++ b/packages/vitec/Configuration/Icons.php @@ -4,6 +4,46 @@ declare(strict_types=1); use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider; return [ + 'vitec-plugin-productlist' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productlist.svg', + ], + 'vitec-plugin-simplecard' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-simplecard.svg', + ], + 'vitec-plugin-productshow' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productshow.svg', + ], + 'vitec-plugin-usecaseshow' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaseshow.svg', + ], + 'vitec-plugin-marketshow' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketshow.svg', + ], + 'vitec-plugin-solutionshow' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-solutionshow.svg', + ], + 'vitec-plugin-usecaselist' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaselist.svg', + ], + 'vitec-plugin-downloadcard' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcard.svg', + ], + 'vitec-plugin-downloadcardcollection' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcardcollection.svg', + ], + 'vitec-plugin-datasheets' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg', + ], 'vitec-cols-50-50' => [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-50-50.svg', @@ -24,6 +64,10 @@ return [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg', ], + 'vitec-container' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-container.svg', + ], 'vitec-ogimage' => [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg', diff --git a/packages/vitec/Configuration/Sets/Vitecset/config.yaml b/packages/vitec/Configuration/Sets/Vitecset/config.yaml index 0e6a3fe..ce40a05 100644 --- a/packages/vitec/Configuration/Sets/Vitecset/config.yaml +++ b/packages/vitec/Configuration/Sets/Vitecset/config.yaml @@ -7,5 +7,4 @@ settings: dependencies: - typo3/fluid-styled-content - friendsoftypo3/headless - - itplusx/headless-container - nb-headless-content-blocks/headless-content-blocks diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript index f302731..e9b7818 100644 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript @@ -8,91 +8,129 @@ config { } } +@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript' + # ============================================================================= -# Headless list-plugin JSON renderers +# Headless list-plugin JSON renderers (TYPO3 v14: each plugin is its own CType) # -# `tt_content.list.20.` renders the element body for that subtype -# (only ever invoked for the matching list_type — safe as-is). -# -# `tt_content.list.fields.content.fields.` is GLOBAL: without scoping it -# would inject into EVERY list plugin's content. Each such field is -# therefore guarded with: -# stdWrap.if { equals.field = list_type ... } -> empty for other plugins -# ifEmptyUnsetKey = 1 -> empty key removed entirely -# (headless JsonContentObject honours ifEmptyUnsetKey: '' / false => unset). +# For every VITEC plugin the renderer's USER output is exposed as a sub-field +# of the standard content-element envelope (`content.`), inheriting all +# default fields (id, type, colPos, categories, appearance, …) from +# lib.contentElement provided by friendsoftypo3/headless. # ============================================================================= -# --- vitec_productlist -------------------------------------------------------- -tt_content.list.20.vitec_productlist = USER -tt_content.list.20.vitec_productlist { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render -} - -tt_content.list.fields.content.fields.products = USER -tt_content.list.fields.content.fields.products { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render - stdWrap.if { - value = vitec_productlist - equals.field = list_type +tt_content { + vitec_productlist < lib.contentElement + vitec_productlist { + fields { + content { + fields { + products = USER + products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render + } + } + } } - ifEmptyUnsetKey = 1 -} -# --- vitec_productshow -------------------------------------------------------- -tt_content.list.20.vitec_productshow = USER -tt_content.list.20.vitec_productshow { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render -} - -tt_content.list.fields.content.fields.product = USER -tt_content.list.fields.content.fields.product { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render - stdWrap.if { - value = vitec_productshow - equals.field = list_type + vitec_productshow < lib.contentElement + vitec_productshow { + fields { + content { + fields { + product = USER + product.userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render + } + } + } } - ifEmptyUnsetKey = 1 -} -# --- vitec_usecaselist -------------------------------------------------------- -tt_content.list.20.vitec_usecaselist = USER -tt_content.list.20.vitec_usecaselist { - userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render -} - -tt_content.list.fields.content.fields.usecases = USER -tt_content.list.fields.content.fields.usecases { - userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render - stdWrap.if { - value = vitec_usecaselist - equals.field = list_type + vitec_usecaselist < lib.contentElement + vitec_usecaselist { + fields { + content { + fields { + usecases = USER + usecases.userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render + } + } + } } - ifEmptyUnsetKey = 1 -} -# --- vitec_usecaseshow -------------------------------------------------------- -tt_content.list.20.vitec_usecaseshow = USER -tt_content.list.20.vitec_usecaseshow { - userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render -} - -tt_content.list.fields.content.fields.usecase = USER -tt_content.list.fields.content.fields.usecase { - userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render - stdWrap.if { - value = vitec_usecaseshow - equals.field = list_type + vitec_usecaseshow < lib.contentElement + vitec_usecaseshow { + fields { + content { + fields { + usecase = USER + usecase.userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render + } + } + } + } + + vitec_marketshow < lib.contentElement + vitec_marketshow { + fields { + content { + fields { + market = USER + market.userFunc = Evomedien\Vitec\UserFunc\MarketShowJsonRenderer->render + } + } + } + } + + vitec_solutionshow < lib.contentElement + vitec_solutionshow { + fields { + content { + fields { + solution = USER + solution.userFunc = Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer->render + } + } + } + } + + vitec_downloadcard < lib.contentElement + vitec_downloadcard { + fields { + content { + fields { + downloadcard = USER + downloadcard.userFunc = Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer->render + } + } + } + } + + vitec_downloadcardcollection < lib.contentElement + vitec_downloadcardcollection { + fields { + content { + fields { + downloadcardcollection = USER + downloadcardcollection.userFunc = Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer->render + } + } + } + } + + vitec_datasheets < lib.contentElement + vitec_datasheets { + fields { + content { + fields { + datasheets = USER + datasheets.userFunc = Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer->render + } + } + } } - ifEmptyUnsetKey = 1 } # Include container (layout) rendering definitions @import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript' -# Ensure headless's content element JSON definitions take precedence over -# fluid_styled_content's HTML definitions. This is loaded at the end of the -# Vitecset to guarantee winning load order. -@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript' - # Include menu (navigation) JSON definitions @import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript' diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 deleted file mode 100755 index 17b9f1a..0000000 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 +++ /dev/null @@ -1,44 +0,0 @@ -config { - pageTitleProviders { - vitec { - provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider - before = record - before = seo - } - } -} - -# Headless: Add products field to vitec_productlist -tt_content.list.20.vitec_productlist = USER -tt_content.list.20.vitec_productlist { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render -} - -# Override the JSON structure for vitec_productlist -tt_content.list.fields.content.fields.products = USER -tt_content.list.fields.content.fields.products { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render -} - -# Headless: Add product field to vitec_productshow -tt_content.list.20.vitec_productshow = USER -tt_content.list.20.vitec_productshow { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render -} - -# Override the JSON structure for vitec_productshow -tt_content.list.fields.content.fields.product = USER -tt_content.list.fields.content.fields.product { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render -} - -# Include container (layout) rendering definitions -@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript' - -# Ensure headless's content element JSON definitions take precedence over -# fluid_styled_content's HTML definitions. This is loaded at the end of the -# Vitecset to guarantee winning load order. -@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript' - -# Include menu (navigation) JSON definitions -@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript' diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 deleted file mode 100755 index 042963f..0000000 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 +++ /dev/null @@ -1,82 +0,0 @@ -config { - pageTitleProviders { - vitec { - provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider - before = record - before = seo - } - } -} - -# ============================================================================= -# Headless list-plugin JSON renderers -# -# `tt_content.list.20.` renders the element body for that subtype -# (only ever invoked for the matching list_type — safe as-is). -# -# `tt_content.list.fields.content.fields.` is GLOBAL: without scoping it -# would inject into EVERY list plugin's content. Each such field is -# therefore guarded with: -# stdWrap.if { equals.field = list_type ... } -> empty for other plugins -# ifEmptyUnsetKey = 1 -> empty key removed entirely -# (headless JsonContentObject honours ifEmptyUnsetKey: '' / false => unset). -# ============================================================================= - -# --- vitec_productlist -------------------------------------------------------- -tt_content.list.20.vitec_productlist = USER -tt_content.list.20.vitec_productlist { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render -} - -tt_content.list.fields.content.fields.products = USER -tt_content.list.fields.content.fields.products { - userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render - stdWrap.if { - value = vitec_productlist - equals.field = list_type - } - ifEmptyUnsetKey = 1 -} - -# --- vitec_productshow -------------------------------------------------------- -tt_content.list.20.vitec_productshow = USER -tt_content.list.20.vitec_productshow { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render -} - -tt_content.list.fields.content.fields.product = USER -tt_content.list.fields.content.fields.product { - userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render - stdWrap.if { - value = vitec_productshow - equals.field = list_type - } - ifEmptyUnsetKey = 1 -} - -# --- vitec_usecaselist -------------------------------------------------------- -tt_content.list.20.vitec_usecaselist = USER -tt_content.list.20.vitec_usecaselist { - userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render -} - -tt_content.list.fields.content.fields.usecases = USER -tt_content.list.fields.content.fields.usecases { - userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render - stdWrap.if { - value = vitec_usecaselist - equals.field = list_type - } - ifEmptyUnsetKey = 1 -} - -# Include container (layout) rendering definitions -@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript' - -# Ensure headless's content element JSON definitions take precedence over -# fluid_styled_content's HTML definitions. This is loaded at the end of the -# Vitecset to guarantee winning load order. -@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript' - -# Include menu (navigation) JSON definitions -@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript' 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 5bd1746..ac8a692 100644 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content.php @@ -1,120 +1,53 @@ 'Vitec: Full Width', 'value' => 'vitec-full-width'], - ['label' => 'Vitec: Centered Container', 'value' => 'vitec-centered'], - ['label' => 'Vitec: Card Style', 'value' => 'vitec-card'], - ['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'], - ['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'], - ] -); + // Add custom frame_class options for Vitec. + $GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'] = array_merge( + $GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'], + [ + ['label' => 'Vitec: Full Width', 'value' => 'vitec-full-width'], + ['label' => 'Vitec: Centered Container', 'value' => 'vitec-centered'], + ['label' => 'Vitec: Card Style', 'value' => 'vitec-card'], + ['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'], + ['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'], + ] + ); +})(); 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 fcb7702..92d2acf --- 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 @@ -47,7 +47,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; --palette--;;hidden, --palette--;;access'; - ExtensionManagementUtility::addPageTSConfig(<<configureContainer( - (new ContainerConfiguration( - 'vitec_container', - $l . 'container.title', - $l . 'container.description', - [ - [ - ['name' => $l . 'container.column', 'colPos' => 220], - ], - ] - )) - ->setIcon('EXT:vitec/Resources/Public/Icons/vitec-container.svg') - ->setGroup('vitec') - ->setSaveAndCloseInNewContentElementWizard(true) - ); - - $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, - tx_vitec_bg_variant, - pi_flexform, - --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'; - - // Bind the Container FlexForm to this CType. - // Core tt_content.pi_flexform uses ds_pointerField = 'list_type,CType', - // so the CType value is a valid data-structure key. - $GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds']['vitec_container'] = - 'FILE:EXT:vitec/Configuration/FlexForms/Container.xml'; - - ExtensionManagementUtility::addPageTSConfig(<< [ + \TYPO3\CMS\Core\Resource\FileType::TEXT->value => [ 'showitem' => ' --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette, --palette--;;filePalette' ], - \TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [ + \TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [ 'showitem' => ' --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette, --palette--;;filePalette' ], - \TYPO3\CMS\Core\Resource\File::FILETYPE_AUDIO => [ + \TYPO3\CMS\Core\Resource\FileType::AUDIO->value => [ 'showitem' => ' --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette, --palette--;;filePalette' ], - \TYPO3\CMS\Core\Resource\File::FILETYPE_VIDEO => [ + \TYPO3\CMS\Core\Resource\FileType::VIDEO->value => [ 'showitem' => ' --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette, --palette--;;filePalette' ], - \TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [ + \TYPO3\CMS\Core\Resource\FileType::APPLICATION->value => [ 'showitem' => ' --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette, --palette--;;filePalette' 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_product.php.bak.20260529111828 b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php.bak.20260529111828 deleted file mode 100755 index 90850fe..0000000 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php.bak.20260529111828 +++ /dev/null @@ -1,541 +0,0 @@ - [ - 'title' => 'VITEC Product', - 'label' => 'title', - 'tstamp' => 'tstamp', - 'crdate' => 'crdate', - 'cruser_id' => 'cruser_id', - 'versioningWS' => true, - 'languageField' => 'sys_language_uid', - 'transOrigPointerField' => 'l10n_parent', - 'transOrigDiffSourceField' => 'l10n_diffsource', - 'delete' => 'deleted', - 'enablecolumns' => [ - 'disabled' => 'hidden', - 'starttime' => 'starttime', - 'endtime' => 'endtime', - ], - 'searchFields' => 'title,slug', - 'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_product.gif', - 'security' => [ - 'ignorePageTypeRestriction' => true, - ], - ], - 'types' => [ - '1' => ['showitem' => 'title, subtitle, slug, teaser, description, applications, highlights, contentelement, contentelementcta, downloads, - --div--;SEO, seotitle, urltitle, seometa, keywords, structureddata, - --div--;Images and Videos, productimage, ogimage, video, - --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, - --div--;Visibility, hideonapp, hideonwebsite, hideondatasheets, hideonproducts, shortcut, shortcutpid, - --div--;Misc, legacy, supportproduct, subproduct, relatedprodukt, - --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' => [ - ['', 0], - ], - 'foreign_table' => 'tx_vitec_domain_model_product', - 'foreign_table_where' => 'AND {#tx_vitec_domain_model_product}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_product}.{#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' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => true - ] - ], - ], - ], - '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 - ] - ], - ], - '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 - ] - ], - ], - 'categories' => [ - 'config' => [ - 'type' => 'category' - ] - ], - 'title' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'required' => true, - 'default' => '' - ], - ], - 'slug' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug.description', - 'config' => [ - 'type' => 'slug', - 'size' => 50, - 'generatorOptions' => [ - 'fields' => ['title'], // TODO: adjust this field to the one you want to use - 'fieldSeparator' => '-', - 'replacements' => [ - '/' => '', - ], - ], - 'fallbackCharacter' => '-', - 'eval' => 'uniqueInPid', - ], - - ], - 'seotitle' => [ - 'exclude' => false, - 'label' => 'SEO-Title', - 'description' => 'SEO specific title, used for sharing options', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'urltitle' => [ - 'exclude' => false, - 'label' => 'URL-Title', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.urltitle.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'video' => [ - 'exclude' => false, - 'label' => 'Product Video - just the Youtube Clip ID', - 'description' => 'Product Video - just the Youtube Clip ID', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'downloads' => [ - 'exclude' => true, - 'label' => 'Downloads', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectMultipleSideBySide', - 'foreign_table' => 'tx_vitec_domain_model_download', - 'MM' => 'tx_vitec_product_download_mm', // Define the MM table for the relation - 'size' => 10, - 'maxitems' => 9999, - 'minitems' => 0, - 'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering - ], - ], - 'structureddata' => [ - 'exclude' => true, - 'label' => 'Structured Data in JSON Format', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'keywords' => [ - 'exclude' => true, - 'label' => 'Keywords', - 'description' => 'Keywords for SEO, separated by commas', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'seometa' => [ - 'exclude' => true, - 'label' => 'SEO Meta Description', - 'description' => 'SEO specific meta description, used for sharing options', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'teaser' => [ - 'exclude' => true, - 'label' => 'Short Teaser (optional)', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - - 'subtitle' => [ - 'exclude' => true, - 'label' => 'Subtitle', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'hideonapp' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on App', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'hideonwebsite' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Website', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'hideonproducts' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Product Section', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'hideondatasheets' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Datasheets Page', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'applications' => [ - 'exclude' => true, - 'label' => 'Applications', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - 'description' => [ - 'exclude' => true, - 'label' => 'Description', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - - 'highlights' => [ - 'exclude' => true, - 'label' => 'Highlights', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - - 'shortcut' => [ - 'exclude' => true, - 'label' => 'Does this Product have a custom Page', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'shortcutpid' => [ - 'exclude' => false, - 'label' => 'Page ID', - 'description' => 'PID of the custom page', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - - 'legacy' => [ - 'exclude' => true, - 'label' => 'Is this a legacy produc?', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'supportproduct' => [ - 'exclude' => true, - 'label' => 'Is this a support product?', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'subproduct' => [ - 'exclude' => true, - 'label' => 'Product is a sub-products', - 'description' => 'This Product will not be shown as a main product', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'relatedprodukt' => [ - 'exclude' => true, - 'label' => 'Related Products', - 'description' => 'Select related products for this product', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectMultipleSideBySide', - 'foreign_table' => 'tx_vitec_domain_model_product', // Reference the same table - 'MM' => 'tx_vitec_product_related_mm', // Define the MM table for the relation - 'size' => 10, - 'maxitems' => 9999, - 'minitems' => 0, - 'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering - ], - ], - 'productimage' => [ - 'exclude' => true, - 'label' => 'Product Image(s)', - 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'sys_file_reference', - 'foreign_field' => 'uid_foreign', - 'foreign_sortby' => 'sorting_foreign', - 'foreign_table_field' => 'tablenames', - 'foreign_match_fields' => [ - 'fieldname' => 'productimage', - ], - 'appearance' => [ - 'collapseAll' => true, - 'levelLinksPosition' => 'top', - 'showSynchronizationLink' => true, - 'showPossibleLocalizationRecords' => true, - 'showAllLocalizationLink' => true, - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - 'filter' => [ - [ - 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', - 'parameters' => [ - 'allowedFileExtensions' => 'jpg,jpeg,png,gif', - ], - ], - ], - 'maxitems' => 10, - 'minitems' => 0, - ], - ], - - 'ogimage' => [ - 'exclude' => true, - 'label' => 'Open Graph 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' => 'ogimage', - ], - 'appearance' => [ - 'collapseAll' => true, - 'levelLinksPosition' => 'top', - 'showSynchronizationLink' => true, - 'showPossibleLocalizationRecords' => true, - 'showAllLocalizationLink' => true, - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - 'filter' => [ - [ - 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', - 'parameters' => [ - 'allowedFileExtensions' => 'jpg,jpeg,png,gif,webp', - ], - ], - ], - 'maxitems' => 1, // Allow only one image - 'minitems' => 0, - ], - ], - - 'contentelement' => [ - 'exclude' => true, - 'label' => 'Select Content Element for Key Features Section', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputLink', - 'softref' => 'typolink', - 'eval' => 'trim', - 'fieldControl' => [ - 'linkPopup' => [ - 'options' => [ - 'title' => 'Select Content Element', - 'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types - 'blindLinkFields' => 'class,params', // Disable unnecessary fields - ], - ], - ], - ], - ], - - 'contentelementcta' => [ - 'exclude' => true, - 'label' => 'Select Content Element for CTA Section', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputLink', - 'softref' => 'typolink', - 'eval' => 'trim', - 'fieldControl' => [ - 'linkPopup' => [ - 'options' => [ - 'title' => 'Select Content Element', - 'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types - 'blindLinkFields' => 'class,params', // Disable unnecessary fields - ], - ], - ], - ], - ], - -/* ----------------------------------------------------- */ - ], -]; \ No newline at end of file diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php.bak.20260529120751 b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php.bak.20260529120751 deleted file mode 100755 index b77d023..0000000 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php.bak.20260529120751 +++ /dev/null @@ -1,576 +0,0 @@ - [ - 'title' => 'VITEC Product', - 'label' => 'title', - 'tstamp' => 'tstamp', - 'crdate' => 'crdate', - 'cruser_id' => 'cruser_id', - 'versioningWS' => true, - 'languageField' => 'sys_language_uid', - 'transOrigPointerField' => 'l10n_parent', - 'transOrigDiffSourceField' => 'l10n_diffsource', - 'delete' => 'deleted', - 'enablecolumns' => [ - 'disabled' => 'hidden', - 'starttime' => 'starttime', - 'endtime' => 'endtime', - ], - 'searchFields' => 'title,slug', - 'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_product.gif', - 'security' => [ - 'ignorePageTypeRestriction' => true, - ], - ], - 'types' => [ - '1' => ['showitem' => 'title, subtitle, slug, teaser, description, applications, highlights, contentelement, contentelementcta, downloads, - --div--;SEO, seotitle, urltitle, seometa, keywords, structureddata, - --div--;Images and Videos, productimage, ogimage, video, videofile, - --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, - --div--;Visibility, hideonapp, hideonwebsite, hideondatasheets, hideonproducts, shortcut, shortcutpid, - --div--;Misc, legacy, supportproduct, subproduct, relatedprodukt, - --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' => [ - ['', 0], - ], - 'foreign_table' => 'tx_vitec_domain_model_product', - 'foreign_table_where' => 'AND {#tx_vitec_domain_model_product}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_product}.{#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' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => true - ] - ], - ], - ], - '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 - ] - ], - ], - '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 - ] - ], - ], - 'categories' => [ - 'config' => [ - 'type' => 'category' - ] - ], - 'title' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'required' => true, - 'default' => '' - ], - ], - 'slug' => [ - 'exclude' => false, - 'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug.description', - 'config' => [ - 'type' => 'slug', - 'size' => 50, - 'generatorOptions' => [ - 'fields' => ['title'], // TODO: adjust this field to the one you want to use - 'fieldSeparator' => '-', - 'replacements' => [ - '/' => '', - ], - ], - 'fallbackCharacter' => '-', - 'eval' => 'uniqueInPid', - ], - - ], - 'seotitle' => [ - 'exclude' => false, - 'label' => 'SEO-Title', - 'description' => 'SEO specific title, used for sharing options', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'urltitle' => [ - 'exclude' => false, - 'label' => 'URL-Title', - 'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.urltitle.description', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'video' => [ - 'exclude' => false, - 'label' => 'Product Video - just the Youtube Clip ID', - 'description' => 'Product Video - just the Youtube Clip ID', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'downloads' => [ - 'exclude' => true, - 'label' => 'Downloads', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectMultipleSideBySide', - 'foreign_table' => 'tx_vitec_domain_model_download', - 'MM' => 'tx_vitec_product_download_mm', // Define the MM table for the relation - 'size' => 10, - 'maxitems' => 9999, - 'minitems' => 0, - 'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering - ], - ], - 'structureddata' => [ - 'exclude' => true, - 'label' => 'Structured Data in JSON Format', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'keywords' => [ - 'exclude' => true, - 'label' => 'Keywords', - 'description' => 'Keywords for SEO, separated by commas', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'seometa' => [ - 'exclude' => true, - 'label' => 'SEO Meta Description', - 'description' => 'SEO specific meta description, used for sharing options', - 'config' => [ - 'type' => 'text', - 'cols' => 40, - 'rows' => 15, - 'eval' => 'trim' - ] - ], - 'teaser' => [ - 'exclude' => true, - 'label' => 'Short Teaser (optional)', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - - 'subtitle' => [ - 'exclude' => true, - 'label' => 'Subtitle', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - 'hideonapp' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on App', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'hideonwebsite' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Website', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'hideonproducts' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Product Section', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'hideondatasheets' => [ - 'exclude' => true, - 'label' => 'If checked - Product will not be shown on Datasheets Page', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'applications' => [ - 'exclude' => true, - 'label' => 'Applications', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - 'description' => [ - 'exclude' => true, - 'label' => 'Description', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - - 'highlights' => [ - 'exclude' => true, - 'label' => 'Highlights', - 'config' => [ - 'type' => 'text', - 'enableRichtext' => 'true', - 'eval' => 'trim', - 'default' => '' - ], - 'defaultExtras' => 'richtext:rte_transform[mode=ts_css]' - ], - - 'shortcut' => [ - 'exclude' => true, - 'label' => 'Does this Product have a custom Page', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'shortcutpid' => [ - 'exclude' => false, - 'label' => 'Page ID', - 'description' => 'PID of the custom page', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim', - 'default' => '' - ], - ], - - 'legacy' => [ - 'exclude' => true, - 'label' => 'Is this a legacy produc?', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'supportproduct' => [ - 'exclude' => true, - 'label' => 'Is this a support product?', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - - 'subproduct' => [ - 'exclude' => true, - 'label' => 'Product is a sub-products', - 'description' => 'This Product will not be shown as a main product', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 0 => '', - 1 => '', - 'invertStateDisplay' => false - ] - ], - ], - ], - 'relatedprodukt' => [ - 'exclude' => true, - 'label' => 'Related Products', - 'description' => 'Select related products for this product', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectMultipleSideBySide', - 'foreign_table' => 'tx_vitec_domain_model_product', // Reference the same table - 'MM' => 'tx_vitec_product_related_mm', // Define the MM table for the relation - 'size' => 10, - 'maxitems' => 9999, - 'minitems' => 0, - 'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering - ], - ], - 'productimage' => [ - 'exclude' => true, - 'label' => 'Product Image(s)', - 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'sys_file_reference', - 'foreign_field' => 'uid_foreign', - 'foreign_sortby' => 'sorting_foreign', - 'foreign_table_field' => 'tablenames', - 'foreign_match_fields' => [ - 'fieldname' => 'productimage', - ], - 'appearance' => [ - 'collapseAll' => true, - 'levelLinksPosition' => 'top', - 'showSynchronizationLink' => true, - 'showPossibleLocalizationRecords' => true, - 'showAllLocalizationLink' => true, - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - 'filter' => [ - [ - 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', - 'parameters' => [ - 'allowedFileExtensions' => 'jpg,jpeg,png,gif', - ], - ], - ], - 'maxitems' => 10, - 'minitems' => 0, - ], - ], - - 'ogimage' => [ - 'exclude' => true, - 'label' => 'Open Graph 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' => 'ogimage', - ], - 'appearance' => [ - 'collapseAll' => true, - 'levelLinksPosition' => 'top', - 'showSynchronizationLink' => true, - 'showPossibleLocalizationRecords' => true, - 'showAllLocalizationLink' => true, - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - 'filter' => [ - [ - 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', - 'parameters' => [ - 'allowedFileExtensions' => 'jpg,jpeg,png,gif,webp', - ], - ], - ], - 'maxitems' => 1, // Allow only one image - 'minitems' => 0, - ], - ], - - 'videofile' => [ - 'exclude' => true, - 'label' => 'Video File (Upload or Local)', - 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'sys_file_reference', - 'foreign_field' => 'uid_foreign', - 'foreign_sortby' => 'sorting_foreign', - 'foreign_table_field' => 'tablenames', - 'foreign_match_fields' => [ - 'fieldname' => 'videofile', - ], - 'appearance' => [ - 'collapseAll' => true, - 'levelLinksPosition' => 'top', - 'showSynchronizationLink' => true, - 'showPossibleLocalizationRecords' => true, - 'showAllLocalizationLink' => true, - ], - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - 'filter' => [ - [ - 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', - 'parameters' => [ - 'allowedFileExtensions' => 'mp4,webm,ogv,mov,m4v', - ], - ], - ], - 'maxitems' => 1, - 'minitems' => 0, - ], - ], - - 'contentelement' => [ - 'exclude' => true, - 'label' => 'Select Content Element for Key Features Section', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputLink', - 'softref' => 'typolink', - 'eval' => 'trim', - 'fieldControl' => [ - 'linkPopup' => [ - 'options' => [ - 'title' => 'Select Content Element', - 'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types - 'blindLinkFields' => 'class,params', // Disable unnecessary fields - ], - ], - ], - ], - ], - - 'contentelementcta' => [ - 'exclude' => true, - 'label' => 'Select Content Element for CTA Section', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputLink', - 'softref' => 'typolink', - 'eval' => 'trim', - 'fieldControl' => [ - 'linkPopup' => [ - 'options' => [ - 'title' => 'Select Content Element', - 'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types - 'blindLinkFields' => 'class,params', // Disable unnecessary fields - ], - ], - ], - ], - ], - -/* ----------------------------------------------------- */ - ], -]; 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 diff --git a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript.bak.20260518150609 b/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript.bak.20260518150609 deleted file mode 100755 index 7effd3a..0000000 --- a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript.bak.20260518150609 +++ /dev/null @@ -1,89 +0,0 @@ -# ============================================================================= -# 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). -# ============================================================================= - -tt_content.vitec_cols_50_50 = JSON -tt_content.vitec_cols_50_50 { - fields { - id = INT - id.field = uid - type = TEXT - type.field = CType - colPos = INT - colPos.field = colPos - - categories = COA - categories { - 10 = CONTENT - 10 { - table = sys_category - select { - pidInList = root - selectFields = sys_category.title - join = sys_category_record_mm on sys_category_record_mm.uid_local = sys_category.uid - where { - field = uid - wrap = AND sys_category_record_mm.tablenames = 'tt_content' AND sys_category_record_mm.uid_foreign=| - } - } - renderObj = TEXT - renderObj { - field = title - wrap = |###BREAK### - } - } - stdWrap.split { - token = ###BREAK### - cObjNum = 1 |*|2|*| 3 - 1 { - current = 1 - stdWrap.wrap = | - } - 2 { - current = 1 - stdWrap.wrap = ,| - } - 3 { - current = 1 - stdWrap.wrap = | - } - } - } - - appearance = JSON - appearance { - fields { - layout = TEXT - layout.field = layout - frameClass = TEXT - frameClass.field = frame_class - spaceBefore = TEXT - spaceBefore.field = space_before_class - spaceAfter = TEXT - spaceAfter.field = space_after_class - } - } - - header = TEXT - header.field = header - subheader = TEXT - subheader.field = subheader - tx_vitec_bg_variant = TEXT - tx_vitec_bg_variant.field = tx_vitec_bg_variant - - items = JSON - items { - dataProcessing { - 10 = Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor - 10.as = items - } - } - } -} - -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 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/page.tsconfig b/packages/vitec/Configuration/page.tsconfig index 312c5d7..af41fb7 100644 --- a/packages/vitec/Configuration/page.tsconfig +++ b/packages/vitec/Configuration/page.tsconfig @@ -13,8 +13,7 @@ mod { title = List of VITEC Products description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_productlist.description tt_content_defValues { - CType = list - list_type = vitec_productlist + CType = vitec_productlist } } productshow { @@ -22,8 +21,7 @@ mod { title = Show single VITEC Product description = Either show a single product coming from a list, OR select a specific product in flexform tt_content_defValues { - CType = list - list_type = vitec_productshow + CType = vitec_productshow } } usecaseshow { @@ -31,8 +29,7 @@ mod { title = Show single VITEC Success Story description = Shows a selected Success Story in a Card with different layouts tt_content_defValues { - CType = list - list_type = vitec_usecaseshow + CType = vitec_usecaseshow } } marketshow { @@ -40,8 +37,7 @@ mod { title = Show single VITEC Market description = Shows a selected Market in a Card with different layouts tt_content_defValues { - CType = list - list_type = vitec_marketshow + CType = vitec_marketshow } } solutionshow { @@ -49,8 +45,7 @@ mod { title = Show single VITEC Solution description = Shows a selected Solution in a Card with different layouts tt_content_defValues { - CType = list - list_type = vitec_solutionshow + CType = vitec_solutionshow } } usecaselist { @@ -58,8 +53,7 @@ mod { title = Show all VITEC Success Stories description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description tt_content_defValues { - CType = list - list_type = vitec_usecaselist + CType = vitec_usecaselist } } downloadcard { @@ -67,8 +61,7 @@ mod { title = Downloadcard for single Download description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description tt_content_defValues { - CType = list - list_type = vitec_downloadcard + CType = vitec_downloadcard } } downloadcardcollection { @@ -76,8 +69,7 @@ mod { title = Downloadcard Collection description = Multiple Downloads in one Card tt_content_defValues { - CType = list - list_type = vitec_downloadcardcollection + CType = vitec_downloadcardcollection } } datasheets { @@ -85,8 +77,7 @@ mod { title = VITEC Datasheets description = Interactive datasheets with filtering and download functionality tt_content_defValues { - CType = list - list_type = vitec_datasheets + CType = vitec_datasheets } } simplecard { @@ -94,8 +85,7 @@ mod { title = Simple Card description = Simple Card (Image, Title, Text, Link) tt_content_defValues { - CType = list - list_type = vitec_simplecard + CType = vitec_simplecard } } diff --git a/packages/vitec/ContentBlocks/ContentElements/card/assets/icon.svg b/packages/vitec/ContentBlocks/ContentElements/card/assets/icon.svg new file mode 100644 index 0000000..fd5a2ef --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/card/assets/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/card/config.yaml b/packages/vitec/ContentBlocks/ContentElements/card/config.yaml new file mode 100644 index 0000000..8f3d2a8 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/card/config.yaml @@ -0,0 +1,186 @@ +name: vitec/card +group: vitec +prefixFields: true +prefixType: vendor + +fields: + # ────────────────────────────────────────────────────────────────────── + - identifier: tab_content + type: Tab + label: Content + + - identifier: eyebrow + type: Text + max: 50 + + - identifier: header + useExistingField: true + required: true + + - identifier: subheader + useExistingField: true + + - identifier: bodytext + useExistingField: true + enableRichtext: true + + - identifier: image + type: File + minitems: 0 + maxitems: 1 + allowed: common-image-types + + - identifier: icon + type: File + minitems: 0 + maxitems: 1 + allowed: svg,png + + - identifier: primary_cta + type: Link + allowedTypes: + - page + - url + - file + - email + + - identifier: primary_cta_label + type: Text + max: 30 + default: 'Learn more' + + - identifier: secondary_cta + type: Link + allowedTypes: + - page + - url + - file + - email + + - identifier: secondary_cta_label + type: Text + max: 30 + + - identifier: card_link + type: Link + allowedTypes: + - page + - url + - file + - email + + # ────────────────────────────────────────────────────────────────────── + - identifier: tab_layout + type: Tab + label: Layout + + - identifier: layout_variant + type: Select + renderType: selectSingle + default: vertical + items: + - label: Vertical (image top, content below) + value: vertical + - label: Horizontal (image left, content right) + value: horizontal + - label: Horizontal Reverse (content left, image right) + value: horizontal_reverse + - label: Image Overlay (text over image) + value: image_overlay + - label: Text Only (no image) + value: text_only + - label: Icon Card (icon focused, text below) + value: icon_card + - label: Feature (large highlighted card) + value: feature + - label: Compact (small card, tight spacing) + value: compact + + - identifier: background_variant + 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 (neutral) + value: light + - label: Dark (neutral) + value: dark + + - identifier: image_aspect_ratio + type: Select + renderType: selectSingle + default: auto + items: + - label: Auto (original) + value: auto + - label: Square (1:1) + value: '1_1' + - label: Landscape (4:3) + value: '4_3' + - label: Widescreen (16:9) + value: '16_9' + - label: Portrait (3:4) + value: '3_4' + + - identifier: text_alignment + type: Select + renderType: selectSingle + default: left + items: + - label: Left + value: left + - label: Center + value: center + - label: Right + value: right + + - identifier: border_style + type: Select + renderType: selectSingle + default: none + items: + - label: None + value: none + - label: Soft (subtle line) + value: soft + - label: Hard (thick line) + value: hard + - label: Dashed + value: dashed + + - identifier: shadow + type: Select + renderType: selectSingle + default: none + items: + - label: None + value: none + - label: Soft + value: soft + - label: Medium + value: medium + - label: Strong + value: strong + + # ────────────────────────────────────────────────────────────────────── + - identifier: tab_advanced + type: Tab + label: Advanced + + - identifier: cssClass + type: Text + max: 100 + + - identifier: debug + type: Checkbox + default: 0 diff --git a/packages/vitec/ContentBlocks/ContentElements/card/language/labels.xlf b/packages/vitec/ContentBlocks/ContentElements/card/language/labels.xlf new file mode 100644 index 0000000..58b954d --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/card/language/labels.xlf @@ -0,0 +1,206 @@ + + + + + + VITEC · Card + + + Multi-purpose card with image / icon, body, two CTAs, layout/background/aspect-ratio/alignment/border/shadow options. + + + + Content + + + Layout + + + Advanced + + + + Eyebrow (small pre-title) + + + Image + + + Primary card image. Optional — leave empty for text-only / icon-only layouts. + + + Icon (SVG or PNG) + + + Small icon used as fallback when no image is set, or as a visual indicator in icon-focused layouts. + + + + Primary CTA Link + + + Primary CTA Button Text + + + Secondary CTA Link (optional) + + + Secondary CTA Button Text (optional) + + + Whole-card link (optional) + + + If set, the whole card becomes clickable. Overrides individual CTAs visually in the frontend. + + + + + + + Layout Variant + + + Vertical (image top, content below) + + + Horizontal (image left, content right) + + + Horizontal Reverse (content left, image right) + + + Image Overlay (text over image) + + + Text Only (no image) + + + Icon Card (icon focused, text below) + + + Feature (large highlighted card) + + + Compact (small card, tight spacing) + + + + + + + Background Variant + + + None + + + Orange + + + Blue + + + Graphite + + + Midnight + + + Light (neutral) + + + Dark (neutral) + + + + + + + Image Aspect Ratio + + + Auto (original) + + + Square (1:1) + + + Landscape (4:3) + + + Widescreen (16:9) + + + Portrait (3:4) + + + + + + + Text Alignment + + + Left + + + Center + + + Right + + + + + + + Border Style + + + None + + + Soft (subtle line) + + + Hard (thick line) + + + Dashed + + + + + + + Shadow + + + None + + + Soft + + + Medium + + + Strong + + + + Additional CSS Class + + + Custom CSS class rendered on the card root element in the frontend. + + + Allow Debug Output + + + When enabled, extra debug fields may be included in the JSON output for this element. + + + + diff --git a/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html new file mode 100644 index 0000000..576085a --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html @@ -0,0 +1,150 @@ + + + + + + +
+ + Thumbnail: image > icon > placeholder + + + + + + + + + + +
+ + 📝 + + + + + +
+
+
+
+
+ +
+
VITEC · Card
+ + +
{data.eyebrow}
+
+ +

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

+ + +
{data.subheader}
+
+ + +
+ {data.bodytext} +
+
+ +
+ + {data.primary_cta_label} → + + + + {data.secondary_cta_label} + + +
+
+ + Settings sidebar with badges +
+ + + + ▤ Vertical + ▥ Horizontal + ▤ Horizontal-Rev + ▣ Image Overlay + 📝 Text Only + ✦ Icon Card + ⭐ Feature + ▫ Compact + {data.layout_variant} + + + + + + + No BG + Orange + Blue + Graphite + Midnight + Light + Dark + {data.background_variant} + + + + + ⛶ {data.image_aspect_ratio -> f:format.raw()} + + + + + + ↔ Center + → Right + {data.text_alignment} + + + + + + ▭ Border: {data.border_style} + + + + ☁ Shadow: {data.shadow} + + + + 🔗 Whole-card link + + + + ⌗ .{data.cssClass} + + + + ⚙ Debug ON + + +
+ +
+ +
+ diff --git a/packages/vitec/ContentBlocks/ContentElements/card/templates/frontend.html b/packages/vitec/ContentBlocks/ContentElements/card/templates/frontend.html new file mode 100644 index 0000000..da9c5f0 --- /dev/null +++ b/packages/vitec/ContentBlocks/ContentElements/card/templates/frontend.html @@ -0,0 +1,3 @@ + + + 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 diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 deleted file mode 100755 index 68279b8..0000000 --- a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 +++ /dev/null @@ -1,91 +0,0 @@ - - - -
- - - VITEC - - - - Section Heading - - - Section Subline - - - - Background Variant - - - None - - - Orange - - - Blue - - - Graphite - - - Midnight - - - - VITEC · Two Columns (50 / 50) - - - Two equal columns - - - - VITEC · Three Columns (33 / 33 / 33) - - - Three equal columns - - - - VITEC · Four Columns (25 / 25 / 25 / 25) - - - Four equal columns - - - - VITEC · Two Columns (66 / 33) - - - Wide main column + narrow sidebar - - - - VITEC · Two Columns (33 / 66) - - - Narrow sidebar + wide main column - - - - Column 1 - - - Column 2 - - - Column 3 - - - Column 4 - - - Main (66%) - - - Sidebar (33%) - - - - diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 deleted file mode 100755 index cbd5a4d..0000000 --- a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 +++ /dev/null @@ -1,110 +0,0 @@ - - - -
- - - VITEC - - - - Section Heading - - - Section Subline - - - - Background Variant - - - None - - - Orange - - - Blue - - - Graphite - - - Midnight - - - - VITEC · Two Columns (50 / 50) - - - Two equal columns - - - - VITEC · Three Columns (33 / 33 / 33) - - - Three equal columns - - - - VITEC · Four Columns (25 / 25 / 25 / 25) - - - Four equal columns - - - - VITEC · Two Columns (66 / 33) - - - Wide main column + narrow sidebar - - - - VITEC · Two Columns (33 / 66) - - - Narrow sidebar + wide main column - - - - VITEC · Container - - - Generic container for other content elements with a custom CSS class - - - Content - - - Container Settings - - - CSS Class - - - Custom CSS class that is rendered on the container in the frontend. - - - - Column 1 - - - Column 2 - - - Column 3 - - - Column 4 - - - Main (66%) - - - Sidebar (33%) - - - - 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/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/composer.json b/packages/vitec/composer.json index a545d44..1665f6f 100644 --- a/packages/vitec/composer.json +++ b/packages/vitec/composer.json @@ -6,7 +6,7 @@ "authors": [], "license": "GPL-2.0-or-later", "require": { - "typo3/cms-core": "^13.4" + "typo3/cms-core": "^13.4 || ^14.0" }, "autoload": { "psr-4": { diff --git a/packages/vitec/ext_emconf.php b/packages/vitec/ext_emconf.php index 32fad32..3c42a2a 100644 --- a/packages/vitec/ext_emconf.php +++ b/packages/vitec/ext_emconf.php @@ -11,7 +11,7 @@ $EM_CONF[$_EXTKEY] = [ 'version' => '1.0.1', 'constraints' => [ 'depends' => [ - 'typo3' => '13.4.0-13.4.99', + 'typo3' => '13.4.0-14.9.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/packages/vitec/ext_localconf.php b/packages/vitec/ext_localconf.php old mode 100755 new mode 100644 index f7fa26b..5da9272 --- a/packages/vitec/ext_localconf.php +++ b/packages/vitec/ext_localconf.php @@ -1,8 +1,6 @@ registerIcon( - 'vitec-plugin-productlist', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productlist.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-productshow', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productshow.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-usecaseshow', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaseshow.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-marketshow', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketshow.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-solutionshow', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-solutionshow.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-usecaselist', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaselist.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-downloadcard', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcard.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-downloadcardcollection', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcardcollection.svg'] - ); - $iconRegistry->registerIcon( - 'vitec-plugin-datasheets', - \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, - ['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg'] - ); - })(); diff --git a/packages/vitec/ext_tables.php b/packages/vitec/ext_tables.php index 563e7c0..dd6109e 100644 --- a/packages/vitec/ext_tables.php +++ b/packages/vitec/ext_tables.php @@ -11,48 +11,6 @@ ExtensionManagementUtility::addToAllTCAtypes( 'categories' ); -// Include Page TSConfig -ExtensionManagementUtility::addPageTSConfig( - '' -); - -// Register the Downloadcard plugin -\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'Vitec', - 'Downloadcard', - 'Download Card' -); - -// Register the Simplecard plugin -\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'Vitec', - 'Simplecard', - 'Simple Card' -); - -// Register the Downloadcard plugin -\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'Vitec', - 'Downloadcardcollection', - 'Download Card Collection' -); - -$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_productlist'] = 'pi_flexform'; -\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue( - 'vitec_productlist', - 'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml' -); - -// Add FlexForm for Downloadcard plugin -$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_downloadcard'] = 'pi_flexform'; -ExtensionManagementUtility::addPiFlexFormValue( - 'vitec_downloadcard', - 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcard.xml' -); - -// Add FlexForm for DownloadcardCollection plugin -$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_downloadcardcollection'] = 'pi_flexform'; -ExtensionManagementUtility::addPiFlexFormValue( - 'vitec_downloadcardcollection', - 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml' -); \ No newline at end of file +// Include Page TSConfig (TYPO3 v14 compatible registration) +$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') + . "\n"; \ No newline at end of file diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql old mode 100755 new mode 100644 diff --git a/packages/vitec/ext_tables.sql.bak.20260529111828 b/packages/vitec/ext_tables.sql.bak.20260529111828 deleted file mode 100755 index 8e6ed68..0000000 --- a/packages/vitec/ext_tables.sql.bak.20260529111828 +++ /dev/null @@ -1,171 +0,0 @@ -CREATE TABLE tx_vitec_domain_model_product ( - 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, - PRIMARY KEY (uid), - urltitle varchar(255) DEFAULT '' NOT NULL, - seotitle varchar(255) DEFAULT '' NOT NULL, - seometa text, - keywords text, - productimage int(11) DEFAULT '0' NOT NULL, - structureddata text, - teaser varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - applications text, - highlights text, - description text, - image INTEGER, - relatedimage INTEGER, - ogimage INTEGER, - legacy tinyint(4) DEFAULT '0' NOT NULL, - supportproduct tinyint(4) DEFAULT '0' NOT NULL, - subproduct tinyint(4) DEFAULT '0' NOT NULL, - cta varchar(255) DEFAULT '' NOT NULL, - sorting1 smallint(5) NOT NULL, - sorting2 smallint(5) NOT NULL, - sorting3 smallint(5) NOT NULL, - sorting4 smallint(5) NOT NULL, - sorting5 smallint(5) NOT NULL, - links text, - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL, - shortcut tinyint(4) DEFAULT '0' NOT NULL, - shortcutpid smallint(5) NOT NULL, - video varchar(255) DEFAULT '' NOT NULL, - key1 varchar(255) DEFAULT '' NOT NULL, - key2 varchar(255) DEFAULT '' NOT NULL, - key3 varchar(255) DEFAULT '' NOT NULL, - apptext1 varchar(255) DEFAULT '' NOT NULL, - apptext2 varchar(255) DEFAULT '' NOT NULL, - apptext3 varchar(255) DEFAULT '' NOT NULL, - productlayout int(11) DEFAULT '0' NOT NULL, - contentelement varchar(255) DEFAULT '' NOT NULL, - contentelementcta varchar(255) DEFAULT '' NOT NULL, - KEY parent (pid) -); -CREATE TABLE tx_vitec_domain_model_download ( - title varchar(255) DEFAULT '' NOT NULL, - slug varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) NOT NULL DEFAULT '', - keywords varchar(255) NOT NULL DEFAULT '', - description text, - file int(11) unsigned NOT NULL DEFAULT '0', - sort1 varchar(255) NOT NULL DEFAULT '', - sort2 varchar(255) NOT NULL DEFAULT '', - sort3 varchar(255) NOT NULL DEFAULT '', - private_download smallint(1) unsigned NOT NULL DEFAULT '0', - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL, - icon varchar(255) NOT NULL DEFAULT '', - filepath varchar(255) NOT NULL DEFAULT '', - fileprefix varchar(255) NOT NULL DEFAULT '', - useolddl smallint(1) unsigned NOT NULL DEFAULT '0' -); - -CREATE TABLE tx_vitec_product_download_mm ( - uid_local int(11) DEFAULT '0' NOT NULL, - uid_foreign int(11) DEFAULT '0' NOT NULL, - sorting int(11) DEFAULT '0' NOT NULL, - sorting_foreign int(11) DEFAULT '0' NOT NULL, - KEY uid_local (uid_local), - KEY uid_foreign (uid_foreign) -); - -CREATE TABLE tx_vitec_product_related_mm ( - uid_local INT(11) NOT NULL, - uid_foreign INT(11) NOT NULL, - sorting INT(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid_local, uid_foreign) -); - -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, - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid) -); - -CREATE TABLE tx_vitec_domain_model_solution ( - 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, - sys_language_uid int(11) DEFAULT '0' NOT NULL, - l10n_parent int(11) DEFAULT '0' NOT NULL, - l10n_diffsource mediumblob, - title varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) DEFAULT '' NOT NULL, - description text, - image int(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid), - KEY language (l10n_parent, sys_language_uid) -); - -CREATE TABLE tx_vitec_domain_model_market ( - 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, - sys_language_uid int(11) DEFAULT '0' NOT NULL, - l10n_parent int(11) DEFAULT '0' NOT NULL, - l10n_diffsource mediumblob, - title varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) DEFAULT '' NOT NULL, - description text, - image int(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid), - KEY language (l10n_parent, sys_language_uid) -); - -ALTER TABLE sys_category -ADD class VARCHAR(255) DEFAULT '' NOT NULL, -ADD filetype VARCHAR(255) DEFAULT '' NOT NULL, -ADD type VARCHAR(255) DEFAULT '' NOT NULL; - - -CREATE TABLE tt_content ( - tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL -); \ No newline at end of file diff --git a/packages/vitec/ext_tables.sql.bak.20260529120751 b/packages/vitec/ext_tables.sql.bak.20260529120751 deleted file mode 100755 index ecc77ff..0000000 --- a/packages/vitec/ext_tables.sql.bak.20260529120751 +++ /dev/null @@ -1,172 +0,0 @@ -CREATE TABLE tx_vitec_domain_model_product ( - 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, - PRIMARY KEY (uid), - urltitle varchar(255) DEFAULT '' NOT NULL, - seotitle varchar(255) DEFAULT '' NOT NULL, - seometa text, - keywords text, - productimage int(11) DEFAULT '0' NOT NULL, - structureddata text, - teaser varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - applications text, - highlights text, - description text, - image INTEGER, - relatedimage INTEGER, - ogimage INTEGER, - legacy tinyint(4) DEFAULT '0' NOT NULL, - supportproduct tinyint(4) DEFAULT '0' NOT NULL, - subproduct tinyint(4) DEFAULT '0' NOT NULL, - cta varchar(255) DEFAULT '' NOT NULL, - sorting1 smallint(5) NOT NULL, - sorting2 smallint(5) NOT NULL, - sorting3 smallint(5) NOT NULL, - sorting4 smallint(5) NOT NULL, - sorting5 smallint(5) NOT NULL, - links text, - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL, - shortcut tinyint(4) DEFAULT '0' NOT NULL, - shortcutpid smallint(5) NOT NULL, - video varchar(255) DEFAULT '' NOT NULL, - key1 varchar(255) DEFAULT '' NOT NULL, - key2 varchar(255) DEFAULT '' NOT NULL, - key3 varchar(255) DEFAULT '' NOT NULL, - apptext1 varchar(255) DEFAULT '' NOT NULL, - apptext2 varchar(255) DEFAULT '' NOT NULL, - apptext3 varchar(255) DEFAULT '' NOT NULL, - productlayout int(11) DEFAULT '0' NOT NULL, - contentelement varchar(255) DEFAULT '' NOT NULL, - contentelementcta varchar(255) DEFAULT '' NOT NULL, - videofile int(11) unsigned DEFAULT '0' NOT NULL, - KEY parent (pid) -); -CREATE TABLE tx_vitec_domain_model_download ( - title varchar(255) DEFAULT '' NOT NULL, - slug varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) NOT NULL DEFAULT '', - keywords varchar(255) NOT NULL DEFAULT '', - description text, - file int(11) unsigned NOT NULL DEFAULT '0', - sort1 varchar(255) NOT NULL DEFAULT '', - sort2 varchar(255) NOT NULL DEFAULT '', - sort3 varchar(255) NOT NULL DEFAULT '', - private_download smallint(1) unsigned NOT NULL DEFAULT '0', - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL, - icon varchar(255) NOT NULL DEFAULT '', - filepath varchar(255) NOT NULL DEFAULT '', - fileprefix varchar(255) NOT NULL DEFAULT '', - useolddl smallint(1) unsigned NOT NULL DEFAULT '0' -); - -CREATE TABLE tx_vitec_product_download_mm ( - uid_local int(11) DEFAULT '0' NOT NULL, - uid_foreign int(11) DEFAULT '0' NOT NULL, - sorting int(11) DEFAULT '0' NOT NULL, - sorting_foreign int(11) DEFAULT '0' NOT NULL, - KEY uid_local (uid_local), - KEY uid_foreign (uid_foreign) -); - -CREATE TABLE tx_vitec_product_related_mm ( - uid_local INT(11) NOT NULL, - uid_foreign INT(11) NOT NULL, - sorting INT(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid_local, uid_foreign) -); - -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, - hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL, - hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid) -); - -CREATE TABLE tx_vitec_domain_model_solution ( - 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, - sys_language_uid int(11) DEFAULT '0' NOT NULL, - l10n_parent int(11) DEFAULT '0' NOT NULL, - l10n_diffsource mediumblob, - title varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) DEFAULT '' NOT NULL, - description text, - image int(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid), - KEY language (l10n_parent, sys_language_uid) -); - -CREATE TABLE tx_vitec_domain_model_market ( - 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, - sys_language_uid int(11) DEFAULT '0' NOT NULL, - l10n_parent int(11) DEFAULT '0' NOT NULL, - l10n_diffsource mediumblob, - title varchar(255) DEFAULT '' NOT NULL, - subtitle varchar(255) DEFAULT '' NOT NULL, - teaser varchar(255) DEFAULT '' NOT NULL, - description text, - image int(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), - KEY parent (pid), - KEY language (l10n_parent, sys_language_uid) -); - -ALTER TABLE sys_category -ADD class VARCHAR(255) DEFAULT '' NOT NULL, -ADD filetype VARCHAR(255) DEFAULT '' NOT NULL, -ADD type VARCHAR(255) DEFAULT '' NOT NULL; - - -CREATE TABLE tt_content ( - tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL -); diff --git a/public/_frontend/assets/index-5XMvTCBl.js b/public/_frontend/assets/index-5XMvTCBl.js new file mode 100644 index 0000000..1bbe027 --- /dev/null +++ b/public/_frontend/assets/index-5XMvTCBl.js @@ -0,0 +1,120 @@ +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));(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 l=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 D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function ee(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var k=/\/+/g;function A(e,t){return typeof e==`object`&&e&&e.key!=null?ee(``+e.key):t.toString(36)}function te(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 ne(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,ne(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+A(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(k,`$&/`)+`/`),ne(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(k,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=l()})),d=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,O());else{var t=n(l);t!==null&&A(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&&A(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var ee=new MessageChannel,k=ee.port2;ee.port1.onmessage=D,O=function(){k.postMessage(null)}}else O=function(){_(D,0)};function A(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,A(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),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}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();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=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1oe||(e.current=ae[oe],ae[oe]=null,oe--)}function le(e,t){oe++,ae[oe]=e.current,e.current=t}var ue=se(null),de=se(null),fe=se(null),pe=se(null);function me(e,t){switch(le(fe,t),le(de,e),le(ue,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}}ce(ue),le(ue,e)}function he(){ce(ue),ce(de),ce(fe)}function ge(e){e.memoizedState!==null&&le(pe,e);var t=ue.current,n=Wd(t,e.type);t!==n&&(le(de,e),le(ue,n))}function _e(e){de.current===e&&(ce(ue),ce(de)),pe.current===e&&(ce(pe),ep._currentValue=ie)}var ve,ye;function be(e){if(ve===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ve=t&&t[1]||``,ye=-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{xe=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?be(n):``}function Ce(e,t){switch(e.tag){case 26:case 27:case 5:return be(e.type);case 16:return be(`Lazy`);case 13:return e.child!==t&&t!==null?be(`Suspense Fallback`):be(`Suspense`);case 19:return be(`SuspenseList`);case 0:case 15:return Se(e.type,!1);case 11:return Se(e.type.render,!1);case 1:return Se(e.type,!0);case 31:return be(`Activity`);default:return``}}function we(e){try{var t=``,n=null;do t+=Ce(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var Te=Object.prototype.hasOwnProperty,Ee=t.unstable_scheduleCallback,De=t.unstable_cancelCallback,Oe=t.unstable_shouldYield,ke=t.unstable_requestPaint,Ae=t.unstable_now,je=t.unstable_getCurrentPriorityLevel,Me=t.unstable_ImmediatePriority,Ne=t.unstable_UserBlockingPriority,Pe=t.unstable_NormalPriority,Fe=t.unstable_LowPriority,Ie=t.unstable_IdlePriority,Le=t.log,Re=t.unstable_setDisableYieldValue,ze=null,Be=null;function Ve(e){if(typeof Le==`function`&&Re(e),Be&&typeof Be.setStrictMode==`function`)try{Be.setStrictMode(ze,e)}catch{}}var He=Math.clz32?Math.clz32:Ge,Ue=Math.log,We=Math.LN2;function Ge(e){return e>>>=0,e===0?32:31-(Ue(e)/We|0)|0}var Ke=256,qe=262144,Je=4194304;function Ye(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 Xe(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=Ye(n))):i=Ye(o):i=Ye(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ye(n))):i=Ye(o)):i=Ye(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 Ze(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Qe(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 $e(){var e=Je;return Je<<=1,!(Je&62914560)&&(Je=4194304),e}function et(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function tt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function nt(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),hn=!1;if(mn)try{var gn={};Object.defineProperty(gn,`passive`,{get:function(){hn=!0}}),window.addEventListener(`test`,gn,gn),window.removeEventListener(`test`,gn,gn)}catch{hn=!1}var _n=null,vn=null,yn=null;function bn(){if(yn)return yn;var e,t=vn,n=t.length,r,i=`value`in _n?_n.value:_n.textContent,a=i.length;for(e=0;e=Qn),tr=` `,nr=!1;function rr(e,t){switch(e){case`keyup`:return Xn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function ir(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ar=!1;function or(e,t){switch(e){case`compositionend`:return ir(t);case`keypress`:return t.which===32?(nr=!0,tr):null;case`textInput`:return e=t.data,e===tr&&nr?null:e;default:return null}}function sr(e,t){if(ar)return e===`compositionend`||!Zn&&rr(e,t)?(e=bn(),yn=vn=_n=null,ar=!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=kr(n)}}function jr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?jr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Mr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Vt(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=Vt(e.document)}return t}function Nr(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 Pr=mn&&`documentMode`in document&&11>=document.documentMode,Fr=null,Ir=null,Lr=null,Rr=!1;function zr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Rr||Fr==null||Fr!==Vt(r)||(r=Fr,`selectionStart`in r&&Nr(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}),Lr&&Or(Lr,r)||(Lr=r,r=Dd(Ir,`onSelect`),0>=o,i-=o,ki=1<<32-He(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),L&&ji(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),L&&ji(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 L&&ji(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)}),L&&ji(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===O&&Aa(l)===r.type){n(e,r.sibling),c=a(r,o.props),La(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=_i(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=gi(o.type,o.key,o.props,null,e.mode,c),La(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=bi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Aa(o),b(e,r,o,c)}if(j(o))return h(e,r,o,c);if(te(o)){if(l=te(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,Ia(o),c);if(o.$$typeof===C)return b(e,r,ia(e,o),c);Ra(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=vi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Fa=0;var i=b(e,t,n,r);return Pa=null,i}catch(t){if(t===wa||t===Ea)throw t;var a=pi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=za(!0),Va=za(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(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 Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,U&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ui(e),li(e,null,n),t}return oi(e,r,t,n),ui(e)}function qa(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,it(e,n)}}function Ja(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 Ya=!1;function Xa(){if(Ya){var e=ha;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!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?(G&f)===f:(r&f)===f){f!==0&&f===ma&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ha=!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),Xl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=M.T,s={};M.T=s,zs(e,!1,t,n);try{var c=i(),l=M.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Rs(e,t,va(c,r),vu(e)):Rs(e,t,r,vu(e))}catch(n){Rs(e,t,{then:function(){},status:`rejected`,reason:n},vu())}finally{N.p=a,o!==null&&s.types!==null&&(o.types=s.types),M.T=o}}function Os(){}function ks(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=As(e).queue;Ds(e,a,t,ie,n===null?Os:function(){return js(e),n(r)})}function As(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ie,baseState:ie,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:ie},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function js(e){var t=As(e);t.next===null&&(t=e.alternate.memoizedState),Rs(e,t.next.queue,{},vu())}function Ms(){return ra(ep)}function Ns(){return Fo().memoizedState}function Ps(){return Fo().memoizedState}function Fs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=vu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(bu(r,t,n),qa(r,t,n)),t={cache:ua()},e.payload=t;return}t=t.return}}function Is(e,t,n){var r=vu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Bs(e)?Vs(t,n):(n=si(e,t,n,r),n!==null&&(bu(n,e,r),Hs(n,t,r)))}function Ls(e,t,n){Rs(e,t,n,vu())}function Rs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Bs(e))Vs(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,Dr(s,o))return oi(e,t,i,0),Hl===null&&ai(),!1}catch{}if(n=si(e,t,i,r),n!==null)return bu(n,e,r),Hs(n,t,r),!0}return!1}function zs(e,t,n,r){if(r={lane:2,revertLane:fd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Bs(e)){if(t)throw Error(i(479))}else t=si(e,n,r,2),t!==null&&bu(t,e,2)}function Bs(e){var t=e.alternate;return e===R||t!==null&&t===R}function Vs(e,t){yo=vo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Hs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,it(e,n)}}var Us={readContext:ra,use:Ro,useCallback:To,useContext:To,useEffect:To,useImperativeHandle:To,useLayoutEffect:To,useInsertionEffect:To,useMemo:To,useReducer:To,useRef:To,useState:To,useDebugValue:To,useDeferredValue:To,useTransition:To,useSyncExternalStore:To,useId:To,useHostTransitionStatus:To,useFormState:To,useActionState:To,useOptimistic:To,useMemoCache:To,useCacheRefresh:To};Us.useEffectEvent=To;var Ws={readContext:ra,use:Ro,useCallback:function(e,t){return Po().memoizedState=[e,t===void 0?null:t],e},useContext:ra,useEffect:ms,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),fs(4194308,4,bs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return fs(4194308,4,e,t)},useInsertionEffect:function(e,t){fs(4,2,e,t)},useMemo:function(e,t){var n=Po();t=t===void 0?null:t;var r=e();if(bo){Ve(!0);try{e()}finally{Ve(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Po();if(n!==void 0){var i=n(t);if(bo){Ve(!0);try{n(t)}finally{Ve(!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=Is.bind(null,R,e),[r.memoizedState,e]},useRef:function(e){var t=Po();return e={current:e},t.memoizedState=e},useState:function(e){e=Xo(e);var t=e.queue,n=Ls.bind(null,R,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Ss,useDeferredValue:function(e,t){return Ts(Po(),e,t)},useTransition:function(){var e=Xo(!1);return e=Ds.bind(null,R,e.queue,!0,!1),Po().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=R,a=Po();if(L){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Hl===null)throw Error(i(349));G&127||Go(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ms(qo.bind(null,r,o,e),[e]),r.flags|=2048,us(9,{destroy:void 0},Ko.bind(null,r,o,n,t),null),n},useId:function(){var e=Po(),t=Hl.identifierPrefix;if(L){var n=Ai,r=ki;n=(r&~(1<<32-He(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=xo++,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[dt]=t,o[ft]=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&&Ic(t)}}return Vc(t),Lc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Ic(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=fe.current,Ui(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ii,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[dt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Nd(e.nodeValue,n)),e||Bi(t,!0)}else e=Hd(e).createTextNode(r),e[dt]=t,t.stateNode=e}return Vc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ui(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[dt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Vc(t),e=!1}else n=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(i(558))}return Vc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ui(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[dt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Vc(t),a=!1}else a=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(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),zc(t,t.updateQueue),Vc(t),null);case 4:return he(),e===null&&Cd(t.stateNode.containerInfo),Vc(t),null;case 10:return Zi(t.type),Vc(t),null;case 19:if(ce(po),r=t.memoizedState,r===null)return Vc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Bc(r,!1);else{if(Yl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=mo(e),o!==null){for(t.flags|=128,Bc(r,!1),e=o.updateQueue,t.updateQueue=e,zc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)hi(n,e),n=n.sibling;return le(po,po.current&1|2),L&&ji(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ae()>ou&&(t.flags|=128,a=!0,Bc(r,!1),t.lanes=4194304)}else{if(!a)if(e=mo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,zc(t,e),Bc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!L)return Vc(t),null}else 2*Ae()-r.renderingStartTime>ou&&n!==536870912&&(t.flags|=128,a=!0,Bc(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?(Vc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ae(),e.sibling=null,n=po.current,le(po,a?n&1|2:n&1),L&&ji(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Vc(t),t.subtreeFlags&6&&(t.flags|=8192)):Vc(t),n=t.updateQueue,n!==null&&zc(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&&ce(ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Zi(la),Vc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Uc(e,t){switch(Pi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zi(la),he(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return _e(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ce(po),null;case 4:return he(),null;case 10:return Zi(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&ce(ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Zi(la),null;case 25:return null;default:return null}}function Wc(e,t){switch(Pi(t),t.tag){case 3:Zi(la),he();break;case 26:case 27:case 5:_e(t);break;case 4:he();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:ce(po);break;case 10:Zi(t.type);break;case 22:case 23:fo(t),io(),e!==null&&ce(ba);break;case 24:Zi(la)}}function V(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){Yu(t,t.return,e)}}function Gc(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){Yu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Yu(t,t.return,e)}}function H(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Yu(e,e.return,t)}}}function Kc(e,t,n){n.props=Zs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Yu(e,t,n)}}function qc(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){Yu(e,t,n)}}function Jc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Yu(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){Yu(e,t,n)}else n.current=null}function Yc(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){Yu(e,e.return,t)}}function Xc(e,t,n){try{var r=e.stateNode;Ld(r,e.type,n,t),r[ft]=t}catch(t){Yu(e,e.return,t)}}function Zc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&$d(e.type)||e.tag===4}function Qc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Zc(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&&$d(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 $c(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=an));else if(r!==4&&(r===27&&$d(e.type)&&(n=e.stateNode,t=null),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,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&&$d(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(el(e,t,n),e=e.sibling;e!==null;)el(e,t,n),e=e.sibling}function tl(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[dt]=e,t[ft]=n}catch(t){Yu(e,e.return,t)}}var nl=!1,rl=!1,il=!1,al=typeof WeakSet==`function`?WeakSet:Set,ol=null;function sl(e,t){if(e=e.containerInfo,Bd=lp,e=Mr(e),Nr(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},lp=!1,ol=t;ol!==null;)if(t=ol,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,ol=e;else for(;ol!==null;){switch(t=ol,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[dt]=e,wt(o),r=o;break a;case`link`:var s=Uf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Ar(s,h),v=Ar(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,M.T=null,n=mu,mu=null;var o=uu,s=fu;if(lu=0,du=uu=null,fu=0,U&6)throw Error(i(331));var c=U;if(U|=4,Ll(o.current),kl(o,o.current,s,n),U=c,ad(0,!1),Be&&typeof Be.onPostCommitFiberRoot==`function`)try{Be.onPostCommitFiberRoot(ze,o)}catch{}return!0}finally{N.p=a,M.T=r,Gu(e,t)}}function Ju(e,t,n){t=Si(n,t),t=rc(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(tt(e,2),id(e))}function Yu(e,t,n){if(e.tag===3)Ju(e,e,n);else for(;t!==null;){if(t.tag===3){Ju(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(cu===null||!cu.has(r))){e=Si(n,e),n=ic(2),r=Ka(t,n,2),r!==null&&(ac(n,r,t,e),tt(r,2),id(r));break}}t=t.return}}function Xu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Vl;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)||(ql=!0,i.add(n),e=Zu.bind(null,e,t,n),t.then(e,e))}function Zu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Hl===e&&(G&n)===n&&(Yl===4||Yl===3&&(G&62914560)===G&&300>Ae()-iu?!(U&2)&&Du(e,0):Ql|=n,eu===G&&(eu=0)),id(e)}function q(e,t){t===0&&(t=$e()),e=ci(e,t),e!==null&&(tt(e,t),id(e))}function J(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),q(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),q(e,n)}function $u(e,t){return Ee(e,t)}var Y=null,X=null,ed=!1,td=!1,nd=!1,rd=0;function id(e){e!==X&&e.next===null&&(X===null?Y=X=e:X=X.next=e),td=!0,ed||(ed=!0,dd())}function ad(e,t){if(!nd&&td){nd=!0;do for(var n=!1,r=Y;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-He(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ud(r,a))}else a=G,a=Xe(r,r===Hl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ze(r,a)||(n=!0,ud(r,a));r=r.next}while(n);nd=!1}}function od(){sd()}function sd(){td=ed=!1;var e=0;rd!==0&&qd()&&(e=rd);for(var t=Ae(),n=null,r=Y;r!==null;){var i=r.next,a=cd(r,t);a===0?(r.next=null,n===null?Y=i:n.next=i,i===null&&(X=n)):(n=r,(e!==0||a&3)&&(td=!0)),r=i}lu!==0&&lu!==5||ad(e,!1),rd!==0&&(rd=0)}function cd(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 Cf(e,t,n){var r=Sf;if(r&&typeof t==`string`&&t){var i=Ut(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),_f.has(i)||(_f.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Id(t,`link`,e),wt(t),r.head.appendChild(t)))}}function wf(e){yf.D(e),Cf(`dns-prefetch`,e,null)}function Tf(e,t){yf.C(e,t),Cf(`preconnect`,e,t)}function Ef(e,t,n){yf.L(e,t,n);var r=Sf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Ut(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Ut(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Ut(n.imageSizes)+`"]`)):i+=`[href="`+Ut(e)+`"]`;var a=i;switch(t){case`style`:a=Mf(e);break;case`script`:a=If(e)}gf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),gf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Nf(a))||t===`script`&&r.querySelector(Lf(a))||(t=r.createElement(`link`),Id(t,`link`,e),wt(t),r.head.appendChild(t)))}}function Df(e,t){yf.m(e,t);var n=Sf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Ut(r)+`"][href="`+Ut(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=If(e)}if(!gf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),gf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Lf(a)))return}r=n.createElement(`link`),Id(r,`link`,e),wt(r),n.head.appendChild(r)}}}function Of(e,t,n){yf.S(e,t,n);var r=Sf;if(r&&e){var i=Ct(r).hoistableStyles,a=Mf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Nf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=gf.get(a))&&Bf(e,n);var c=o=r.createElement(`link`);wt(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,zf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function kf(e,t){yf.X(e,t);var n=Sf;if(n&&e){var r=Ct(n).hoistableScripts,i=If(e),a=r.get(i);a||(a=n.querySelector(Lf(i)),a||(e=h({src:e,async:!0},t),(t=gf.get(i))&&Vf(e,t),a=n.createElement(`script`),wt(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){yf.M(e,t);var n=Sf;if(n&&e){var r=Ct(n).hoistableScripts,i=If(e),a=r.get(i);a||(a=n.querySelector(Lf(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=gf.get(i))&&Vf(e,t),a=n.createElement(`script`),wt(a),Id(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function jf(e,t,n,r){var a=(a=fe.current)?vf(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=Mf(n.href),n=Ct(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=Mf(n.href);var o=Ct(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(Nf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),gf.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},gf.set(e,n),o||Ff(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=If(n),n=Ct(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 Mf(e){return`href="`+Ut(e)+`"`}function Nf(e){return`link[rel="stylesheet"][`+e+`]`}function Pf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Ff(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),wt(t),e.head.appendChild(t))}function If(e){return`[src="`+Ut(e)+`"]`}function Lf(e){return`script[async]`+e}function Rf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Ut(n.href)+`"]`);if(r)return t.instance=r,wt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),wt(r),Id(r,`style`,a),zf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Mf(n.href);var o=e.querySelector(Nf(a));if(o)return t.state.loading|=4,t.instance=o,wt(o),o;r=Pf(n),(a=gf.get(a))&&Bf(r,a),o=(e.ownerDocument||e).createElement(`link`),wt(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,zf(o,n.precedence,e),t.instance=o;case`script`:return o=If(n.src),(a=e.querySelector(Lf(o)))?(t.instance=a,wt(a),a):(r=n,(a=gf.get(o))&&(r=h({},n),Vf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),wt(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,zf(r,n.precedence,e));return t.instance}function zf(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 Gf(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 Kf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function qf(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=Mf(r.href),a=t.querySelector(Nf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Xf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,wt(a);return}a=t.ownerDocument||t,r=Pf(r),(i=gf.get(i))&&Bf(r,i),a=a.createElement(`link`),wt(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=Xf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Jf=0;function Yf(e,t){return e.stylesheets&&e.count===0&&Qf(e,e.stylesheets),0Jf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Xf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Qf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Zf=null;function Qf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Zf=new Map,t.forEach($f,e),Zf=null,Xf.call(e))}function $f(e,t){if(!(t.state.loading&4)){var n=Zf.get(e);if(n)var r=n.get(null);else{n=new Map,Zf.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=h()})),_=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}}})),v=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}}})),y=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))(T||{}),E={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(T),O={accesskey:`accessKey`,charset:`charSet`,class:`className`,contenteditable:`contentEditable`,contextmenu:`contextMenu`,"http-equiv":`httpEquiv`,itemprop:`itemProp`,tabindex:`tabIndex`},ee=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`},te=(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=te(e,`title`),n=te(e,A.TITLE_TEMPLATE);if(Array.isArray(t)&&(t=t.join(``)),n&&t)return n.replace(/%s/g,()=>t);let r=te(e,A.DEFAULT_TITLE);return t||r||void 0},re=e=>te(e,A.ON_CHANGE_CLIENT_STATE)||(()=>{}),j=(e,t)=>t.filter(t=>t[e]!==void 0).map(t=>t[e]).reduce((e,t)=>({...e,...t}),{}),M=(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&&N(`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:M([`href`],e),bodyAttributes:j(`bodyAttributes`,e),defer:te(e,A.DEFER),encode:te(e,A.ENCODE_SPECIAL_CHARACTERS),htmlAttributes:j(`htmlAttributes`,e),linkTags:ie(`link`,[`rel`,`href`],e),metaTags:ie(`meta`,[`name`,`charset`,`http-equiv`,`property`,`itemprop`],e),noscriptTags:ie(`noscript`,[`innerHTML`],e),onChangeClientState:re(e),scriptTags:ie(`script`,[`src`,`innerHTML`],e),styleTags:ie(`style`,[`cssText`],e),title:ne(e),titleAttributes:j(`titleAttributes`,e),prioritizeSeoTags:ae(e,A.PRIORITIZE_SEO_TAGS)}),se=e=>Array.isArray(e)?e.join(``):e,ce=(e,t)=>{let n=Object.keys(e);for(let r=0;rArray.isArray(e)?e.reduce((e,n)=>(ce(n,t)?e.priority.push(n):e.default.push(n),e),{priority:[],default:[]}):{default:e,priority:[]},ue=(e,t)=>({...e,[t]:void 0}),de=[`noscript`,`script`,`style`],fe=(e,t=!0)=>t===!1?String(e):String(e).replace(/&/g,`&`).replace(//g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),pe=e=>Object.keys(e).reduce((t,n)=>{let r=e[n]===void 0?`${n}`:`${n}="${e[n]}"`;return t?`${t} ${r}`:r},``),me=(e,t,n,r)=>{let i=pe(n),a=se(t);return i?`<${e} ${k}="true" ${i}>${fe(a,r)}`:`<${e} ${k}="true">${fe(a,r)}`},he=(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}="${fe(i[t],n)}"`;return e?`${e} ${r}`:r},``),o=i.innerHTML||i.cssText||``;return`${t}<${e} ${k}="true" ${a}${de.indexOf(e)===-1?`/>`:`>${o}`}`},``),ge=(e,t={})=>Object.keys(e).reduce((t,n)=>{let r=O[n];return t[r||n]=e[n],t},t),_e=(e,t,n)=>{let r=ge(n,{key:t,[k]:!0});return[x.createElement(`title`,r,t)]},ve=(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]}),x.createElement(e,r)}),ye=(e,t,n=!0)=>{switch(e){case`title`:return{toComponent:()=>_e(e,t.title,t.titleAttributes),toString:()=>me(e,t.title,t.titleAttributes,n)};case`bodyAttributes`:case`htmlAttributes`:return{toComponent:()=>ge(t),toString:()=>pe(t)};default:return{toComponent:()=>ve(e,t),toString:()=>he(e,t,n)}}},be=({metaTags:e,linkTags:t,scriptTags:n,encode:r})=>{let i=le(e,E.meta),a=le(t,E.link),o=le(n,E.script);return{priorityMethods:{toComponent:()=>[...ve(`meta`,i.priority),...ve(`link`,a.priority),...ve(`script`,o.priority)],toString:()=>`${ye(`meta`,i.priority,r)} ${ye(`link`,a.priority,r)} ${ye(`script`,o.priority,r)}`},metaTags:i.default,linkTags:a.default,scriptTags:o.default}},xe=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}=be(e)),{priority:p,base:ye(`base`,t,r),bodyAttributes:ye(`bodyAttributes`,n,r),htmlAttributes:ye(`htmlAttributes`,i,r),link:ye(`link`,u,r),meta:ye(`meta`,d,r),noscript:ye(`noscript`,a,r),script:ye(`script`,f,r),style:ye(`style`,o,r),title:ye(`title`,{title:s,titleAttributes:c},r)}},Se=[],Ce=!!(typeof window<`u`&&window.document&&window.document.createElement),we=class{instances=[];canUseDOM=Ce;context;value={setHelmet:e=>{this.context.helmet=e},helmetInstances:{get:()=>this.canUseDOM?Se:this.instances,add:e=>{(this.canUseDOM?Se:this.instances).push(e)},remove:e=>{let t=(this.canUseDOM?Se:this.instances).indexOf(e);(this.canUseDOM?Se:this.instances).splice(t,1)}}};constructor(e,t){this.context=e,this.canUseDOM=t||!1,t||(e.helmet=xe({baseTag:[],bodyAttributes:{},encodeSpecialCharacters:!0,htmlAttributes:{},linkTags:[],metaTags:[],noscriptTags:[],scriptTags:[],styleTags:[],title:``,titleAttributes:{}}))}},Te=parseInt(`19.2.0`.split(`.`)[0],10)>=19,Ee=x.createContext({}),De=class e extends x.Component{static canUseDOM=Ce;helmetData;constructor(t){super(t),Te?this.helmetData=null:this.helmetData=new we(this.props.context||{},e.canUseDOM)}render(){return Te?x.createElement(x.Fragment,null,this.props.children):x.createElement(Ee.Provider,{value:this.helmetData.value},this.props.children)}},Oe=(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}},ke=(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(`,`))},Ae=(e,t)=>{e!==void 0&&document.title!==e&&(document.title=se(e)),ke(`title`,t)},je=(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;ke(`body`,r),ke(`html`,i),Ae(d,f);let p={baseTag:Oe(`base`,n),linkTags:Oe(`link`,a),metaTags:Oe(`meta`,o),noscriptTags:Oe(`noscript`,s),scriptTags:Oe(`script`,l),styleTags:Oe(`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)},Me=null,Ne=e=>{Me&&cancelAnimationFrame(Me),e.defer?Me=requestAnimationFrame(()=>{je(e,()=>{Me=null})}):(je(e),Me=null)},Pe=class extends x.Component{rendered=!1;shouldComponentUpdate(e){return!(0,w.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}));De.canUseDOM?Ne(r):xe&&(n=xe(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}},Fe=[],Ie=e=>{let t={};for(let n of Object.keys(e))t[ee[n]||n]=e[n];return t},Le=e=>{let t={};for(let n of Object.keys(e)){let r=O[n];t[r||n]=e[n]}return t},Re=(e,t)=>{if(!Ce)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)},ze=()=>{let e={},t={};for(let n of Fe){let{htmlAttributes:r,bodyAttributes:i}=n.props;r&&Object.assign(e,Ie(r)),i&&Object.assign(t,Ie(i))}Re(`html`,e),Re(`body`,t)},Be=class extends x.Component{componentDidMount(){Fe.push(this),ze()}componentDidUpdate(){ze()}componentWillUnmount(){let e=Fe.indexOf(this);e!==-1&&Fe.splice(e,1),ze()}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 x.createElement(`title`,Le(t),e)}renderBase(){let{base:e}=this.props;return e?x.createElement(`base`,Le(e)):null}renderMeta(){let{meta:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>x.createElement(`meta`,{key:t,...Le(e)}))}renderLink(){let{link:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>x.createElement(`link`,{key:t,...Le(e)}))}renderScript(){let{script:e}=this.props;return!e||!Array.isArray(e)?null:e.map((e,t)=>{let{innerHTML:n,...r}=e,i=Le(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),x.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=Le(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),x.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=Le(r);return n&&(i.dangerouslySetInnerHTML={__html:n}),x.createElement(`noscript`,{key:t,...i})})}render(){return x.createElement(x.Fragment,null,this.renderTitle(),this.renderBase(),this.renderMeta(),this.renderLink(),this.renderScript(),this.renderStyle(),this.renderNoscript())}},Ve=class extends x.Component{static defaultProps={defer:!0,encodeSpecialCharacters:!0,prioritizeSeoTags:!1};shouldComponentUpdate(e){return!(0,S.default)(ue(this.props,`helmetData`),ue(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,C.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,C.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 x.Children.forEach(e,e=>{if(!e||!e.props)return;let{children:r,...i}=e.props,a=Object.keys(i).reduce((e,t)=>(e[ee[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 we)&&(r=new we(r.context,!0),delete n.helmetData),Te?x.createElement(Be,{...n}):r?x.createElement(Pe,{...n,context:r.value}):x.createElement(Ee.Consumer,null,e=>x.createElement(Pe,{...n,context:e}))}},He=`modulepreload`,Ue=function(e){return`/_frontend/`+e},We={};const Ge=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=Ue(t,n),t in We)return;We[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`:He,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 Ke=`popstate`;function qe(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function Je(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return et(``,{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:tt(t)}return rt(t,n,null,e)}function Ye(e={}){function t(e,t){let{pathname:n=`/`,search:r=``,hash:i=``}=nt(e.location.hash.substring(1));return!n.startsWith(`/`)&&!n.startsWith(`.`)&&(n=`/`+n),et(``,{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:tt(t))}function r(e,t){Ze(e.pathname.charAt(0)===`/`,`relative pathnames are not supported in hash history.push(${JSON.stringify(t)})`)}return rt(t,n,r,e)}function Xe(e,t){if(e===!1||e==null)throw Error(t)}function Ze(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function Qe(){return Math.random().toString(36).substring(2,10)}function $e(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 et(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?nt(t):t,state:n,key:t&&t.key||r||Qe(),mask:i}}function tt({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 nt(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 rt(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=qe(e)?e:et(h.location,e,t);n&&n(r,e),l=u()+1;let d=$e(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=qe(e)?e:et(h.location,e,t);n&&n(r,e),l=u();let i=$e(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 it(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(Ke,d),c=e,()=>{i.removeEventListener(Ke,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 it(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),Xe(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:tt(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function at(e,t,n=`/`){return ot(e,t,n,!1)}function ot(e,t,n,r,i){let a=Tt((typeof t==`string`?nt(t):t).pathname||`/`,n);if(a==null)return null;let o=i??ct(e),s=null,c=wt(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;Xe(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=Pt([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(Xe(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),lt(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:yt(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of ut(e.path))a(e,t,!0,n)}),t}function ut(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=ut(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 dt(e){e.sort((e,t)=>e.score===t.score?bt(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ft=/^:[\w-]+$/,pt=3,mt=2,ht=1,gt=10,_t=-2,vt=e=>e===`*`;function yt(e,t){let n=e.split(`/`),r=n.length;return n.some(vt)&&(r+=_t),t&&(r+=mt),n.filter(e=>!vt(e)).reduce((e,t)=>e+(ft.test(t)?pt:t===``?ht:gt),r)}function bt(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 xt(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 Ct(e,t=!1,n=!0){Ze(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 wt(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return Ze(!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 Tt(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 Et=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Dt(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?nt(e):e,a;return n?(n=Nt(n),a=n.startsWith(`/`)?Ot(n.substring(1),`/`):Ot(n,t)):a=t,{pathname:a,search:Lt(r),hash:Rt(i)}}function Ot(e,t){let n=Ft(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function kt(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 At(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function jt(e){let t=At(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Mt(e,t,n,r=!1){let i;typeof e==`string`?i=nt(e):(i={...e},Xe(!i.pathname||!i.pathname.includes(`?`),kt(`?`,`pathname`,`search`,i)),Xe(!i.pathname||!i.pathname.includes(`#`),kt(`#`,`pathname`,`hash`,i)),Xe(!i.search||!i.search.includes(`#`),kt(`#`,`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=Dt(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Nt=e=>e.replace(/\/\/+/g,`/`),Pt=e=>Nt(e.join(`/`)),Ft=e=>e.replace(/\/+$/,``),It=e=>Ft(e).replace(/^\/*/,`/`),Lt=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Rt=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,zt=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 Bt(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Vt(e){return Pt(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ht=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ut(e,t){let n=e;if(typeof n!=`string`||!Et.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ht)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=Tt(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{Ze(!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 Wt=x.createContext(null);Wt.displayName=`DataRouter`;var Gt=x.createContext(null);Gt.displayName=`DataRouterState`;var Kt=x.createContext(!1);function qt(){return x.useContext(Kt)}var Jt=x.createContext({isTransitioning:!1});Jt.displayName=`ViewTransition`;var Yt=x.createContext(new Map);Yt.displayName=`Fetchers`;var Xt=x.createContext(null);Xt.displayName=`Await`;var Zt=x.createContext(null);Zt.displayName=`Navigation`;var Qt=x.createContext(null);Qt.displayName=`Location`;var $t=x.createContext({outlet:null,matches:[],isDataRoute:!1});$t.displayName=`Route`;var en=x.createContext(null);en.displayName=`RouteError`;var tn=`REACT_ROUTER_ERROR`,nn=`REDIRECT`,rn=`ROUTE_ERROR_RESPONSE`;function an(e){if(e.startsWith(`${tn}:${nn}:{`))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 on(e){if(e.startsWith(`${tn}:${rn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new zt(t.status,t.statusText,t.data)}catch{}}function sn(e,{relative:t}={}){Xe(cn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(Zt),{hash:i,pathname:a,search:o}=mn(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Pt([n,a])),r.createHref({pathname:s,search:o,hash:i})}function cn(){return x.useContext(Qt)!=null}function ln(){return Xe(cn(),`useLocation() may be used only in the context of a component.`),x.useContext(Qt).location}var un=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function dn(e){x.useContext(Zt).static||x.useLayoutEffect(e)}function fn(){let{isDataRoute:e}=x.useContext($t);return e?Mn():pn()}function pn(){Xe(cn(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(Wt),{basename:t,navigator:n}=x.useContext(Zt),{matches:r}=x.useContext($t),{pathname:i}=ln(),a=JSON.stringify(jt(r)),o=x.useRef(!1);return dn(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(Ze(o.current,un),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Mt(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Pt([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}x.createContext(null);function mn(e,{relative:t}={}){let{matches:n}=x.useContext($t),{pathname:r}=ln(),i=JSON.stringify(jt(n));return x.useMemo(()=>Mt(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function hn(e,t,n){Xe(cn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(Zt),{matches:i}=x.useContext($t),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||``;Pn(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=ln(),d;if(t){let e=typeof t==`string`?nt(t):t;Xe(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})):at(e,{pathname:p});Ze(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Ze(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=Sn(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Pt([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Pt([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Qt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function gn(){let e=jn(),t=Bt(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=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var _n=x.createElement(gn,null),vn=class extends x.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=on(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement($t.Provider,{value:this.props.routeContext},x.createElement(en.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(bn,{error:e},t):t}};vn.contextType=Kt;var yn=new WeakMap;function bn({children:e,error:t}){let{basename:n}=x.useContext(Zt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=an(t.digest);if(e){let r=yn.get(t);if(r)throw r;let i=Ut(e.location,n);if(Ht&&!yn.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 yn.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function xn({routeContext:e,match:t,children:n}){let r=x.useContext(Wt);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement($t.Provider,{value:e},n)}function Sn(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);Xe(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:Vt(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||_n,o&&(s<0&&c===0?(Pn(`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?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(xn,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(vn,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function Cn(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function wn(e){let t=x.useContext(Wt);return Xe(t,Cn(e)),t}function Tn(e){let t=x.useContext(Gt);return Xe(t,Cn(e)),t}function En(e){let t=x.useContext($t);return Xe(t,Cn(e)),t}function Dn(e){let t=En(e),n=t.matches[t.matches.length-1];return Xe(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function On(){return Dn(`useRouteId`)}function kn(){return Tn(`useNavigation`).navigation}function An(){let{matches:e,loaderData:t}=Tn(`useMatches`);return x.useMemo(()=>e.map(e=>st(e,t)),[e,t])}function jn(){let e=x.useContext(en),t=Tn(`useRouteError`),n=Dn(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Mn(){let{router:e}=wn(`useNavigate`),t=Dn(`useNavigate`),n=x.useRef(!1);return dn(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{Ze(n.current,un),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Nn={};function Pn(e,t,n){!t&&!Nn[e]&&(Nn[e]=!0,Ze(!1,n))}x.useOptimistic,x.memo(Fn);function Fn({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return hn(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function In({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){Xe(!cn(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=nt(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=Tt(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 Ze(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:x.createElement(Zt.Provider,{value:c},x.createElement(Qt.Provider,{children:t,value:h}))}var Ln=`get`,Rn=`application/x-www-form-urlencoded`;function zn(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Bn(e){return zn(e)&&e.tagName.toLowerCase()===`button`}function Vn(e){return zn(e)&&e.tagName.toLowerCase()===`form`}function Hn(e){return zn(e)&&e.tagName.toLowerCase()===`input`}function Un(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Wn(e,t){return e.button===0&&(!t||t===`_self`)&&!Un(e)}var Gn=null;function Kn(){if(Gn===null)try{new FormData(document.createElement(`form`),0),Gn=!1}catch{Gn=!0}return Gn}var qn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Jn(e){return e!=null&&!qn.has(e)?(Ze(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Rn}"`),null):e}function Yn(e,t){let n,r,i,a,o;if(Vn(e)){let o=e.getAttribute(`action`);r=o?Tt(o,t):null,n=e.getAttribute(`method`)||Ln,i=Jn(e.getAttribute(`enctype`))||Rn,a=new FormData(e)}else if(Bn(e)||Hn(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a