VITEC headless v14: 5-bug fix unifies plugin renderers + cleanup
Resolves a cascade of TYPO3 v14 breaking changes that left every VITEC plugin's JSON output silently empty: 1. `list_type` column dropped — all renderer page-discovery queries now filter by `CType = 'vitec_X'`. 2. `#[AsAllowedCallable]` attribute added to every public render() — without it v14 throws AllowedCallableException, which headless silently filters via its "Oops, an error occurred!" guard. 3. `$GLOBALS['TSFE']->id` is null inside JSON cObj context — page id now read from the `frontend.page.information` request attribute (TSFE fallback kept for legacy entry points). 4. page.tsconfig wizard items migrated to `CType = vitec_X` directly (legacy `CType=list, list_type=...` no longer exists in v14). 5. ContainerChildrenProcessor + ContentElementResolver dispatch the PLUGIN_RENDERERS map by CType (primary) with list_type fallback. Beyond the bug fix this commit also contains: - Datasheets renderer rewritten to "products with newest datasheet" semantics (filtered via Download.hideondatasheets). - New vitec/card content block — multi-purpose card with backend preview and layout/background/aspect/alignment/border/shadow options. - New vitec_container CType (b13 single-column container, FlexForm cssClass propagated into the JSON envelope). - ContentElementResolver service for contentelement/contentelementcta link resolution; ContainerChildrenProcessor for nested plugin resolution inside b13 containers. - Vitecset setup.typoscript: ContentElement import moved to top so lib.contentElement is defined before VITEC's tt_content blocks. - Cleanup: 98 .bak.<timestamp> debugging backups removed; *.bak.* and /public/_assets_install/ added to .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -111,3 +111,9 @@ CLAUDE.local.md
|
|||||||
.continue/
|
.continue/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Working backups created by the SSHFS-deploy workflow
|
||||||
|
*.bak.*
|
||||||
|
|
||||||
|
# TYPO3 auto-generated public assets
|
||||||
|
/public/_assets_install/
|
||||||
@@ -14,4 +14,8 @@ $debugMode = $request->getAttribute('site')->getSettings()->get('vitec.debugMode
|
|||||||
|
|
||||||
|
|
||||||
# Custom Frames:
|
# Custom Frames:
|
||||||
/usr/home/vitecevo/public_html/live/packages/vitec/Configuration/TCA/Overrides/sys_category.php
|
/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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "typo3/cms-base-distribution",
|
"name": "typo3/cms-base-distribution",
|
||||||
"description" : "TYPO3 CMS Base Distribution",
|
"description": "TYPO3 CMS Base Distribution",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"config": {
|
"config": {
|
||||||
@@ -17,47 +17,44 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"b13/container": "^3.1",
|
"b13/container": "^3.1",
|
||||||
"b13/typo3-updater": "^1.1",
|
"b13/typo3-updater": "^1.1",
|
||||||
"co-stack/logs": "^5.3",
|
|
||||||
"evomedien/vitec": "^1.0.1",
|
"evomedien/vitec": "^1.0.1",
|
||||||
"friendsoftypo3/content-blocks": "^1.3",
|
"friendsoftypo3/content-blocks": "^2.4",
|
||||||
"friendsoftypo3/headless": "^4.2",
|
"friendsoftypo3/headless": "^5.0@rc",
|
||||||
"itplusx/headless-container": "^3.0",
|
"netzbewegung/nb-headless-content-blocks": "^0.0.23",
|
||||||
"netzbewegung/nb-headless-content-blocks": "^0.0.21",
|
"typo3/cms-backend": "^14.3",
|
||||||
"nitsan/ns-license": "^13.0",
|
"typo3/cms-belog": "^14.3",
|
||||||
"typo3/cms-backend": "^13.4",
|
"typo3/cms-beuser": "^14.3",
|
||||||
"typo3/cms-belog": "^13.4",
|
"typo3/cms-core": "^14.3",
|
||||||
"typo3/cms-beuser": "^13.4",
|
"typo3/cms-dashboard": "^14.3",
|
||||||
"typo3/cms-core": "^13.4",
|
"typo3/cms-extbase": "^14.3",
|
||||||
"typo3/cms-dashboard": "^13.4",
|
"typo3/cms-extensionmanager": "^14.3",
|
||||||
"typo3/cms-extbase": "^13.4",
|
"typo3/cms-felogin": "^14.3",
|
||||||
"typo3/cms-extensionmanager": "^13.4",
|
"typo3/cms-filelist": "^14.3",
|
||||||
"typo3/cms-felogin": "^13.4",
|
"typo3/cms-fluid": "^14.3",
|
||||||
"typo3/cms-filelist": "^13.4",
|
"typo3/cms-fluid-styled-content": "^14.3",
|
||||||
"typo3/cms-fluid": "^13.4",
|
"typo3/cms-form": "^14.3",
|
||||||
"typo3/cms-fluid-styled-content": "^13.4",
|
"typo3/cms-frontend": "^14.3",
|
||||||
"typo3/cms-form": "^13.4",
|
"typo3/cms-impexp": "^14.3",
|
||||||
"typo3/cms-frontend": "^13.4",
|
"typo3/cms-info": "^14.3",
|
||||||
"typo3/cms-impexp": "^13.4",
|
"typo3/cms-install": "^14.3",
|
||||||
"typo3/cms-info": "^13.4",
|
"typo3/cms-reactions": "^14.3",
|
||||||
"typo3/cms-install": "^13.4",
|
"typo3/cms-recycler": "^14.3",
|
||||||
"typo3/cms-reactions": "^13.4",
|
"typo3/cms-rte-ckeditor": "^14.3",
|
||||||
"typo3/cms-recycler": "^13.4",
|
"typo3/cms-scheduler": "^14.3",
|
||||||
"typo3/cms-rte-ckeditor": "^13.4",
|
"typo3/cms-seo": "^14.3",
|
||||||
"typo3/cms-scheduler": "^13.4",
|
"typo3/cms-sys-note": "^14.3",
|
||||||
"typo3/cms-seo": "^13.4",
|
"typo3/cms-tstemplate": "^14.3",
|
||||||
"typo3/cms-setup": "^13.4",
|
"typo3/cms-viewpage": "^14.3",
|
||||||
"typo3/cms-sys-note": "^13.4",
|
"typo3/cms-webhooks": "^14.3"
|
||||||
"typo3/cms-tstemplate": "^13.4",
|
|
||||||
"typo3/cms-viewpage": "^13.4",
|
|
||||||
"typo3/cms-webhooks": "^13.4",
|
|
||||||
"w3code/w3c-categorymanager": "^13.4"
|
|
||||||
},
|
},
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"name": "t3planet",
|
"name": "t3planet",
|
||||||
"type": "composer",
|
"type": "composer",
|
||||||
"url": "https://composer.t3planet.cloud",
|
"url": "https://composer.t3planet.cloud",
|
||||||
"only": ["nitsan/ns_theme_reactbootstrap"]
|
"only": [
|
||||||
|
"nitsan/ns_theme_reactbootstrap"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "path",
|
"type": "path",
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ dependencies:
|
|||||||
- typo3/seo-sitemap
|
- typo3/seo-sitemap
|
||||||
- friendsoftypo3/headless
|
- friendsoftypo3/headless
|
||||||
- friendsoftypo3/headless-mixed
|
- friendsoftypo3/headless-mixed
|
||||||
- itplusx/headless-container
|
|
||||||
- nb-headless-content-blocks/headless-content-blocks
|
- nb-headless-content-blocks/headless-content-blocks
|
||||||
- evomedien/vitecset
|
- evomedien/vitecset
|
||||||
- vitec/content-blocks-bundle
|
- vitec/content-blocks-bundle
|
||||||
- vitec/cta-banner
|
- vitec/cta-banner
|
||||||
- vitec/herosection
|
- vitec/herosection
|
||||||
|
- vitec/card
|
||||||
frontendBase: ''
|
frontendBase: ''
|
||||||
headless: 1
|
headless: 1
|
||||||
languages:
|
languages:
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\DataProcessing;
|
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\Connection;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
@@ -15,6 +22,11 @@ use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
|
|||||||
* { id, type, colPos, sorting, appearance, data }
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
* `data` only contains non-empty content-relevant fields.
|
* `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.
|
* Exception-safe.
|
||||||
*/
|
*/
|
||||||
final class ContainerChildrenProcessor implements DataProcessorInterface
|
final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||||
@@ -27,7 +39,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
|
|
||||||
/** Technical / system / TCA-default fields — never sent to frontend. */
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
private const SYSTEM_FIELDS = [
|
private const SYSTEM_FIELDS = [
|
||||||
// versioning / language / workspace / housekeeping
|
|
||||||
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
'l10n_source', 'l10n_state', 'l10n_parent',
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
't3_origuid', 'tx_impexp_origuid',
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
@@ -38,8 +49,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
'editlock', 'sorting_foreign', 'rowDescription',
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
'spaceBefore', 'spaceAfter', // legacy
|
'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',
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
'bullets_type', 'cols',
|
'bullets_type', 'cols',
|
||||||
'table_delimiter', 'table_enclosure', 'table_header_position',
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
@@ -53,6 +62,22 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
'header_layout',
|
'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(
|
public function process(
|
||||||
ContentObjectRenderer $cObj,
|
ContentObjectRenderer $cObj,
|
||||||
array $contentObjectConfiguration,
|
array $contentObjectConfiguration,
|
||||||
@@ -124,6 +149,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
$data[$field] = $this->castValue($field, $value);
|
$data[$field] = $this->castValue($field, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve nested VITEC list-plugins to their headless JSON.
|
||||||
|
$this->resolvePluginData($record, $data);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int)$record['uid'],
|
'id' => (int)$record['uid'],
|
||||||
'type' => (string)$record['CType'],
|
'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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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
|
private function isEmpty(mixed $value): bool
|
||||||
{
|
{
|
||||||
return $value === null
|
return $value === null
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
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 Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||||
|
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect container children grouped by colPos and emit a lean, transport-
|
||||||
|
* ready structure. Each child is normalised to:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
* `data` only contains non-empty content-relevant fields.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter', // legacy
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Fields whose 0/empty value is still meaningful. */
|
||||||
|
private const KEEP_IF_ZERO = [
|
||||||
|
'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'],
|
||||||
|
'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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
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\Connection;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
@@ -70,6 +75,11 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
|
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
|
||||||
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
|
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
|
||||||
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
'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(
|
public function process(
|
||||||
@@ -171,13 +181,17 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
*/
|
*/
|
||||||
private function resolvePluginData(array $record, array &$data): void
|
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'] ?? '');
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
|
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
|
||||||
$renderer = GeneralUtility::makeInstance($rendererClass);
|
$renderer = GeneralUtility::makeInstance($rendererClass);
|
||||||
|
|
||||||
if (!method_exists($renderer, 'renderForRecord')) {
|
if (!method_exists($renderer, 'renderForRecord')) {
|
||||||
|
|||||||
@@ -1,922 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\Domain\Model;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
|
||||||
use TYPO3\CMS\Extbase\Domain\Model\Category;
|
|
||||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the "VITEC" Extension for TYPO3 CMS.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please read the
|
|
||||||
* LICENSE.txt file that was distributed with this source code.
|
|
||||||
*
|
|
||||||
* (c) 2025
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product
|
|
||||||
*/
|
|
||||||
class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* title
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @TYPO3\CMS\Extbase\Annotation\Validate("NotEmpty")
|
|
||||||
*/
|
|
||||||
protected $title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* slug
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $slug;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* urltitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $urltitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* seotitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $seotitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* seometa
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $seometa = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* keywords
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $keywords = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* structureddata
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $structureddata = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* teaser
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $teaser = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* subtitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $subtitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* video
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $video = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonapp
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonapp = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonwebsite
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonwebsite = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideondatasheets
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideondatasheets = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonproducts
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonproducts = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* applications
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $applications = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* description
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* highlights
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $highlights = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shortcut
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $shortcut = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shortcutpid
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $shortcutpid = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* legacy
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $legacy = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* supportproduct
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $supportproduct = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* subproduct
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $subproduct = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ObjectStorage<Category>
|
|
||||||
*/
|
|
||||||
protected $categories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product images
|
|
||||||
*
|
|
||||||
* @var ObjectStorage<FileReference>
|
|
||||||
* @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<Category>
|
|
||||||
*/
|
|
||||||
public function getCategories()
|
|
||||||
{
|
|
||||||
return $this->categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the categories
|
|
||||||
*
|
|
||||||
* @param ObjectStorage<Category> $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<FileReference>
|
|
||||||
*/
|
|
||||||
public function getProductimage()
|
|
||||||
{
|
|
||||||
return $this->productimage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the product images
|
|
||||||
*
|
|
||||||
* @param ObjectStorage<FileReference> $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;
|
|
||||||
}
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,952 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\Domain\Model;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
|
||||||
use TYPO3\CMS\Extbase\Domain\Model\Category;
|
|
||||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the "VITEC" Extension for TYPO3 CMS.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please read the
|
|
||||||
* LICENSE.txt file that was distributed with this source code.
|
|
||||||
*
|
|
||||||
* (c) 2025
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product
|
|
||||||
*/
|
|
||||||
class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* title
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @TYPO3\CMS\Extbase\Annotation\Validate("NotEmpty")
|
|
||||||
*/
|
|
||||||
protected $title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* slug
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $slug;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* urltitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $urltitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* seotitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $seotitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* seometa
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $seometa = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* keywords
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $keywords = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* structureddata
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $structureddata = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* teaser
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $teaser = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* subtitle
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $subtitle = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* video
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $video = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonapp
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonapp = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonwebsite
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonwebsite = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideondatasheets
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideondatasheets = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hideonproducts
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $hideonproducts = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* applications
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $applications = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* description
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* highlights
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $highlights = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shortcut
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $shortcut = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shortcutpid
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $shortcutpid = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* legacy
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $legacy = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* supportproduct
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $supportproduct = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* subproduct
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $subproduct = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ObjectStorage<Category>
|
|
||||||
*/
|
|
||||||
protected $categories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product images
|
|
||||||
*
|
|
||||||
* @var ObjectStorage<FileReference>
|
|
||||||
* @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<Category>
|
|
||||||
*/
|
|
||||||
public function getCategories()
|
|
||||||
{
|
|
||||||
return $this->categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the categories
|
|
||||||
*
|
|
||||||
* @param ObjectStorage<Category> $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<FileReference>
|
|
||||||
*/
|
|
||||||
public function getProductimage()
|
|
||||||
{
|
|
||||||
return $this->productimage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the product images
|
|
||||||
*
|
|
||||||
* @param ObjectStorage<FileReference> $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;
|
|
||||||
}
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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 TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,10 +68,12 @@ final class ContentElementResolver
|
|||||||
/**
|
/**
|
||||||
* Resolve a typolink string to a normalised tt_content JSON element.
|
* 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://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)
|
||||||
* - "<numeric>" (legacy: bare tt_content uid)
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
* - everything else → null (page links etc.)
|
* - everything else → null (pure page links etc.)
|
||||||
*
|
*
|
||||||
* @return array<string,mixed>|null
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
@@ -155,33 +157,47 @@ final class ContentElementResolver
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the tt_content uid from a typolink string.
|
* 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
|
private static function extractTtContentUid(string $link): int
|
||||||
{
|
{
|
||||||
|
// Form 1: bare numeric uid
|
||||||
if (ctype_digit($link)) {
|
if (ctype_digit($link)) {
|
||||||
return (int)$link;
|
return (int)$link;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!str_starts_with($link, 't3://record')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = parse_url($link);
|
$parts = parse_url($link);
|
||||||
if (empty($parts['query'])) {
|
if (!is_array($parts)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = [];
|
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
|
||||||
parse_str((string)$parts['query'], $params);
|
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
|
||||||
|
$params = [];
|
||||||
$identifier = (string)($params['identifier'] ?? '');
|
parse_str((string)$parts['query'], $params);
|
||||||
$uid = (int)($params['uid'] ?? 0);
|
$identifier = (string)($params['identifier'] ?? '');
|
||||||
|
$uid = (int)($params['uid'] ?? 0);
|
||||||
if ($identifier !== 'tt_content' || $uid <= 0) {
|
if ($identifier === 'tt_content' && $uid > 0) {
|
||||||
return 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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 TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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 TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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 TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
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 TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
|
||||||
|
* Product.contentelement / Product.contentelementcta) to the JSON
|
||||||
|
* representation of the referenced tt_content element.
|
||||||
|
*
|
||||||
|
* Mirrors the shape produced by
|
||||||
|
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
*
|
||||||
|
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
|
||||||
|
* just like container children, so a referenced productlist / productshow /
|
||||||
|
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
|
||||||
|
*
|
||||||
|
* Exception-safe: every public entry point returns `null` on any failure
|
||||||
|
* so the surrounding JSON output stays clean.
|
||||||
|
*/
|
||||||
|
final class ContentElementResolver
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter',
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const KEEP_IF_ZERO = ['header_layout'];
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - "<numeric>" (legacy: bare tt_content uid)
|
||||||
|
* - everything else → null (pure page links etc.)
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|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<string,mixed> $record
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
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\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
@@ -63,6 +68,11 @@ final class ContentElementResolver
|
|||||||
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
|
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
|
||||||
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
|
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
|
||||||
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
'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
|
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'] ?? '');
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
|
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
|
||||||
$renderer = GeneralUtility::makeInstance($rendererClass);
|
$renderer = GeneralUtility::makeInstance($rendererClass);
|
||||||
|
|
||||||
if (!method_exists($renderer, 'renderForRecord')) {
|
if (!method_exists($renderer, 'renderForRecord')) {
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render the Datasheets plugin as JSON.
|
||||||
|
*
|
||||||
|
* Lists every product that has at least one datasheet, with the LATEST
|
||||||
|
* datasheet attached. "Datasheet" = a Download linked to the product via
|
||||||
|
* tx_vitec_product_download_mm with hideondatasheets = 0. "Latest" is
|
||||||
|
* the download with the highest tstamp.
|
||||||
|
*
|
||||||
|
* The plugin is global — the page on which it is placed is not used as
|
||||||
|
* a filter. FlexForm settings (showFilter, showSearch, itemsPerPage)
|
||||||
|
* are passed through for the frontend UI to honour.
|
||||||
|
*
|
||||||
|
* Exception-safe. Returns '' on failure / nothing to render.
|
||||||
|
*/
|
||||||
|
class DatasheetsJsonRenderer
|
||||||
|
{
|
||||||
|
#[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');
|
||||||
|
|
||||||
|
$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<string,mixed> $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<int,array{product_uid:int,datasheet_uid:int}>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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<string,mixed>|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<string,mixed> $product
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $download
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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<string,mixed>|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'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render the Downloadcard plugin as JSON for headless output.
|
||||||
|
*
|
||||||
|
* Behaviour:
|
||||||
|
* - settings.download set -> 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<string,mixed> $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<string,mixed>|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<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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<string,mixed> $download
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render the Downloadcardcollection plugin as JSON.
|
||||||
|
*
|
||||||
|
* FlexForm-driven (no controller action):
|
||||||
|
* - settings.download : comma-separated tx_vitec_domain_model_download UIDs
|
||||||
|
* (selectMultipleSideBySide).
|
||||||
|
* - settings.image : optional header image (inline FAL on the
|
||||||
|
* tt_content row, fieldname=image).
|
||||||
|
* - settings.layout / magstyle / magheader / magtext / maglink / adddetaillink
|
||||||
|
* are passed through under "settings".
|
||||||
|
*
|
||||||
|
* Exception-safe. Returns '' when nothing to render.
|
||||||
|
*/
|
||||||
|
class DownloadcardcollectionJsonRenderer
|
||||||
|
{
|
||||||
|
#[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');
|
||||||
|
|
||||||
|
$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<string,mixed> $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<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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<string,mixed> $download
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render a single market as JSON for headless output.
|
||||||
|
*
|
||||||
|
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
|
||||||
|
* one specific tt_content row, reused by ContainerChildrenProcessor and
|
||||||
|
* ContentElementResolver. Exception-safe.
|
||||||
|
*/
|
||||||
|
class MarketShowJsonRenderer
|
||||||
|
{
|
||||||
|
#[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_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<string,mixed> $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<string,mixed> $market
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed>|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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -25,9 +27,30 @@ use TYPO3\CMS\Extbase\Service\ImageService;
|
|||||||
*/
|
*/
|
||||||
class ProductListJsonRenderer
|
class ProductListJsonRenderer
|
||||||
{
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
public function render(string $content, array $conf): string
|
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)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('tt_content');
|
->getQueryBuilderForTable('tt_content');
|
||||||
@@ -37,7 +60,7 @@ class ProductListJsonRenderer
|
|||||||
->from('tt_content')
|
->from('tt_content')
|
||||||
->where(
|
->where(
|
||||||
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
$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('deleted', 0),
|
||||||
$queryBuilder->expr()->eq('hidden', 0)
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
)
|
)
|
||||||
@@ -45,13 +68,13 @@ class ProductListJsonRenderer
|
|||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
if (empty($contentElements)) {
|
if (empty($contentElements)) {
|
||||||
// Not a product-list page: emit nothing so headless removes the key.
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->renderForRecord($contentElements[0]);
|
return $this->renderForRecord($contentElements[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render exactly the given tt_content row (the product-list plugin element).
|
* Render exactly the given tt_content row (the product-list plugin element).
|
||||||
* Exception-safe: returns '' on any failure.
|
* Exception-safe: returns '' on any failure.
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render product list as JSON for headless
|
|
||||||
*/
|
|
||||||
class ProductListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
// IMPORTANT: Headless creates new cObj contexts when rendering JSON fields,
|
|
||||||
// losing the current content element context. Also, Extbase repositories don't work
|
|
||||||
// in UserFunc context because the full Extbase framework isn't bootstrapped.
|
|
||||||
//
|
|
||||||
// Solution: Query tt_content directly to find the plugin configuration,
|
|
||||||
// then use direct database queries for products instead of Extbase repositories.
|
|
||||||
|
|
||||||
// Get current page UID
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render product list as JSON for headless
|
|
||||||
*/
|
|
||||||
class ProductListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
// IMPORTANT: Headless creates new cObj contexts when rendering JSON fields,
|
|
||||||
// losing the current content element context. Also, Extbase repositories don't work
|
|
||||||
// in UserFunc context because the full Extbase framework isn't bootstrapped.
|
|
||||||
//
|
|
||||||
// Solution: Query tt_content directly to find the plugin configuration,
|
|
||||||
// then use direct database queries for products instead of Extbase repositories.
|
|
||||||
|
|
||||||
// Get current page UID
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $product Associative DB row of tx_vitec_domain_model_product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render product list as JSON for headless.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (used for top-level list plugins via
|
|
||||||
* TypoScript). `renderForRecord()` processes one specific tt_content row and
|
|
||||||
* is used both internally and by ContainerChildrenProcessor to resolve a
|
|
||||||
* product-list plugin nested inside a b13 container.
|
|
||||||
*/
|
|
||||||
class ProductListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,507 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render product list as JSON for headless.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (used for top-level list plugins via
|
|
||||||
* TypoScript). `renderForRecord()` processes one specific tt_content row and
|
|
||||||
* is used both internally and by ContainerChildrenProcessor to resolve a
|
|
||||||
* product-list plugin nested inside a b13 container.
|
|
||||||
*/
|
|
||||||
class ProductListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,507 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render product list as JSON for headless.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (used for top-level list plugins via
|
|
||||||
* TypoScript). `renderForRecord()` processes one specific tt_content row and
|
|
||||||
* is used both internally and by ContainerChildrenProcessor to resolve a
|
|
||||||
* product-list plugin nested inside a b13 container.
|
|
||||||
*/
|
|
||||||
class ProductListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
@@ -23,11 +25,32 @@ use Doctrine\DBAL\ParameterType;
|
|||||||
*/
|
*/
|
||||||
class ProductShowJsonRenderer
|
class ProductShowJsonRenderer
|
||||||
{
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
public function render(string $content, array $conf): string
|
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');
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
$contentElements = $queryBuilder
|
$contentElements = $queryBuilder
|
||||||
@@ -35,7 +58,7 @@ class ProductShowJsonRenderer
|
|||||||
->from('tt_content')
|
->from('tt_content')
|
||||||
->where(
|
->where(
|
||||||
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
$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('deleted', 0),
|
||||||
$queryBuilder->expr()->eq('hidden', 0)
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
)
|
)
|
||||||
@@ -49,6 +72,7 @@ class ProductShowJsonRenderer
|
|||||||
return $this->renderForRecord($contentElements[0]);
|
return $this->renderForRecord($contentElements[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render exactly the given tt_content row (the product-show plugin element).
|
* Render exactly the given tt_content row (the product-show plugin element).
|
||||||
* Exception-safe: returns '' on any failure.
|
* Exception-safe: returns '' on any failure.
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render single product data as JSON for headless output
|
|
||||||
*/
|
|
||||||
class ProductShowJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)$GLOBALS['TSFE']->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render single product data as JSON for headless output
|
|
||||||
*/
|
|
||||||
class ProductShowJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)$GLOBALS['TSFE']->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<string,mixed> $product Associative DB row of tx_vitec_domain_model_product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,470 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render a single product as JSON for headless output.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (top-level plugin via TypoScript).
|
|
||||||
* `renderForRecord()` processes one specific tt_content row and is reused
|
|
||||||
* by ContainerChildrenProcessor for product-show plugins nested in a
|
|
||||||
* b13 container.
|
|
||||||
*/
|
|
||||||
class ProductShowJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render a single product as JSON for headless output.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (top-level plugin via TypoScript).
|
|
||||||
* `renderForRecord()` processes one specific tt_content row and is reused
|
|
||||||
* by ContainerChildrenProcessor for product-show plugins nested in a
|
|
||||||
* b13 container.
|
|
||||||
*/
|
|
||||||
class ProductShowJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
|
||||||
use TYPO3\CMS\Core\Resource\FileReference;
|
|
||||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render a single product as JSON for headless output.
|
|
||||||
*
|
|
||||||
* `render()` performs page discovery (top-level plugin via TypoScript).
|
|
||||||
* `renderForRecord()` processes one specific tt_content row and is reused
|
|
||||||
* by ContainerChildrenProcessor for product-show plugins nested in a
|
|
||||||
* b13 container.
|
|
||||||
*/
|
|
||||||
class ProductShowJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $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<string,mixed> $product
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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<string,mixed>|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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
@@ -12,106 +14,115 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
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
|
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
|
||||||
* UserFunc context, so tt_content and the usecase record are queried directly.
|
* one specific tt_content row, reused by ContainerChildrenProcessor and
|
||||||
* Exception-safe: returns '' on any failure / when not applicable so headless
|
* ContentElementResolver. Exception-safe.
|
||||||
* `ifEmptyUnsetKey` drops the key entirely.
|
|
||||||
*/
|
*/
|
||||||
class UsecaseShowJsonRenderer
|
class SolutionShowJsonRenderer
|
||||||
{
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
public function render(string $content, array $conf): string
|
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<string,mixed> $contentElement
|
||||||
|
*/
|
||||||
|
public function renderForRecord(array $contentElement): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
$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);
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
$settings = $flexFormData['settings'] ?? [];
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
$usecaseUid = (int)($settings['usecase'] ?? 0);
|
$solutionUid = (int)($settings['solution'] ?? 0);
|
||||||
$layout = (string)($settings['layout'] ?? 'default');
|
$layout = (string)($settings['layout'] ?? 'default');
|
||||||
$debugMode = (bool)($settings['debug'] ?? false);
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
// Fall back to route / query parameter (uid or slug)
|
// Fallback: route / query parameter (numeric uid only — Market has no slug)
|
||||||
if (!$usecaseUid) {
|
if (!$solutionUid) {
|
||||||
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
$usecaseParam = $routeParams['tx_vitec_usecaseshow']['usecase'] ?? null;
|
$solutionParam = $routeParams['tx_vitec_solutionshow']['solution'] ?? null;
|
||||||
|
if ($solutionParam && is_numeric($solutionParam)) {
|
||||||
if ($usecaseParam) {
|
$solutionUid = (int)$solutionParam;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$usecaseUid) {
|
if (!$solutionUid) {
|
||||||
return $debugMode
|
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
|
$solutionQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
|
||||||
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
|
||||||
|
|
||||||
$usecase = $usecaseQb
|
$solution = $solutionQb
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('tx_vitec_domain_model_usecase')
|
->from('tx_vitec_domain_model_solution')
|
||||||
->where(
|
->where(
|
||||||
$usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
$solutionQb->expr()->eq('uid', $solutionQb->createNamedParameter($solutionUid, ParameterType::INTEGER)),
|
||||||
$usecaseQb->expr()->eq('deleted', 0),
|
$solutionQb->expr()->eq('deleted', 0),
|
||||||
$usecaseQb->expr()->eq('hidden', 0)
|
$solutionQb->expr()->eq('hidden', 0)
|
||||||
)
|
)
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAssociative();
|
->fetchAssociative();
|
||||||
|
|
||||||
if (!$usecase) {
|
if (!$solution) {
|
||||||
return $debugMode
|
return $debugMode
|
||||||
? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]])
|
? json_encode(['error' => 'Solution not found', 'debug' => ['solutionUid' => $solutionUid]])
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = [
|
$response = [
|
||||||
'usecase' => $this->serializeUsecase($usecase),
|
'solution' => $this->serializeSolution($solution),
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
@@ -121,7 +132,7 @@ class UsecaseShowJsonRenderer
|
|||||||
if ($debugMode) {
|
if ($debugMode) {
|
||||||
$response['debug'] = [
|
$response['debug'] = [
|
||||||
'pageId' => $pageId,
|
'pageId' => $pageId,
|
||||||
'usecaseUid' => $usecaseUid,
|
'solutionUid' => $solutionUid,
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
];
|
];
|
||||||
@@ -134,39 +145,30 @@ class UsecaseShowJsonRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a single usecase DB row. Structurally identical to
|
* @param array<string,mixed> $solution
|
||||||
* UsecaseListJsonRenderer::serializeUsecase() so list and detail
|
|
||||||
* endpoints expose the same usecase schema.
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $usecase
|
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
protected function serializeUsecase(array $usecase): array
|
protected function serializeSolution(array $solution): array
|
||||||
{
|
{
|
||||||
$uid = (int)$usecase['uid'];
|
$uid = (int)$solution['uid'];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'uid' => $uid,
|
'uid' => $uid,
|
||||||
'title' => (string)($usecase['title'] ?? ''),
|
'title' => (string)($solution['title'] ?? ''),
|
||||||
'slug' => (string)($usecase['slug'] ?? ''),
|
'subtitle' => (string)($solution['subtitle'] ?? ''),
|
||||||
'subtitle' => (string)($usecase['subtitle'] ?? ''),
|
'teaser' => (string)($solution['teaser'] ?? ''),
|
||||||
'teaser' => (string)($usecase['teaser'] ?? ''),
|
'description' => (string)($solution['description'] ?? ''),
|
||||||
'description' => (string)($usecase['description'] ?? ''),
|
'categories' => $this->getSolutionCategories($uid),
|
||||||
'singlepid' => (string)($usecase['singlepid'] ?? ''),
|
'image' => $this->getSolutionImage($uid),
|
||||||
'hideonapp' => (bool)($usecase['hideonapp'] ?? false),
|
|
||||||
'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false),
|
|
||||||
'categories' => $this->getUsecaseCategories($uid),
|
|
||||||
'caseimage' => $this->getUsecaseImage($uid, 'caseimage'),
|
|
||||||
'logoimage' => $this->getUsecaseImage($uid, 'logoimage'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a single FAL image field (caseimage / logoimage) with srcset.
|
* Resolve the single image FAL reference for a market (fieldname=image).
|
||||||
*
|
*
|
||||||
* @return array<string,mixed>|null
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
|
protected function getSolutionImage(int $solutionUid): ?array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_file_reference');
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
@@ -175,9 +177,9 @@ class UsecaseShowJsonRenderer
|
|||||||
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
->from('sys_file_reference', 'sfr')
|
->from('sys_file_reference', 'sfr')
|
||||||
->where(
|
->where(
|
||||||
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)),
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_solution', ParameterType::STRING)),
|
||||||
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('image', ParameterType::STRING)),
|
||||||
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($solutionUid, ParameterType::INTEGER)),
|
||||||
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
)
|
)
|
||||||
@@ -231,10 +233,7 @@ class UsecaseShowJsonRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function getSolutionCategories(int $solutionUid): array
|
||||||
* Get categories for a usecase (resolved sys_category records).
|
|
||||||
*/
|
|
||||||
protected function getUsecaseCategories(int $usecaseUid): array
|
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_category');
|
->getQueryBuilderForTable('sys_category');
|
||||||
@@ -247,12 +246,12 @@ class UsecaseShowJsonRenderer
|
|||||||
'sys_category_record_mm',
|
'sys_category_record_mm',
|
||||||
'mm',
|
'mm',
|
||||||
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
'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 = ' .
|
' AND mm.fieldname = ' .
|
||||||
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
)
|
)
|
||||||
->where(
|
->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.deleted', 0),
|
||||||
$queryBuilder->expr()->eq('c.hidden', 0)
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
)
|
)
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
@@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService;
|
|||||||
*/
|
*/
|
||||||
class UsecaseListJsonRenderer
|
class UsecaseListJsonRenderer
|
||||||
{
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
public function render(string $content, array $conf): string
|
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');
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
$contentElements = $ttContentQb
|
$contentElements = $queryBuilder
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('tt_content')
|
->from('tt_content')
|
||||||
->where(
|
->where(
|
||||||
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)),
|
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaselist', ParameterType::STRING)),
|
||||||
$ttContentQb->expr()->eq('deleted', 0),
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
$ttContentQb->expr()->eq('hidden', 0)
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
)
|
)
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
@@ -46,6 +69,7 @@ class UsecaseListJsonRenderer
|
|||||||
return $this->renderForRecord($contentElements[0]);
|
return $this->renderForRecord($contentElements[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $contentElement
|
* @param array<string,mixed> $contentElement
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
||||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserFunc to render the usecase list as JSON for headless output.
|
|
||||||
*
|
|
||||||
* Mirrors ProductListJsonRenderer: Extbase repositories don't work in a
|
|
||||||
* UserFunc context (framework not bootstrapped), so tt_content and the
|
|
||||||
* usecase records are queried directly. Exception-safe: returns an empty
|
|
||||||
* string on any failure so headless `ifEmptyUnsetKey` can drop the key.
|
|
||||||
*/
|
|
||||||
class UsecaseListJsonRenderer
|
|
||||||
{
|
|
||||||
public function render(string $content, array $conf): string
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->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<string,mixed> $usecase
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
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<string,mixed>|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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\UserFunc;
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
@@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService;
|
|||||||
*/
|
*/
|
||||||
class UsecaseShowJsonRenderer
|
class UsecaseShowJsonRenderer
|
||||||
{
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
public function render(string $content, array $conf): string
|
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');
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
$contentElements = $ttContentQb
|
$contentElements = $queryBuilder
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('tt_content')
|
->from('tt_content')
|
||||||
->where(
|
->where(
|
||||||
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)),
|
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)),
|
||||||
$ttContentQb->expr()->eq('deleted', 0),
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
$ttContentQb->expr()->eq('hidden', 0)
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
)
|
)
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
@@ -46,6 +69,7 @@ class UsecaseShowJsonRenderer
|
|||||||
return $this->renderForRecord($contentElements[0]);
|
return $this->renderForRecord($contentElements[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $contentElement
|
* @param array<string,mixed> $contentElement
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ final class BackendLayoutDataProvider implements DataProviderInterface
|
|||||||
'preFooter' => 'Pre-Footer',
|
'preFooter' => 'Pre-Footer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return 'vitec';
|
||||||
|
}
|
||||||
|
|
||||||
public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection)
|
public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection)
|
||||||
{
|
{
|
||||||
foreach (self::LAYOUT_MAP as $value => $info) {
|
foreach (self::LAYOUT_MAP as $value => $info) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use TYPO3\CMS\Backend\View\BackendLayoutView;
|
|||||||
* Overrides BackendLayoutView so the page module always reflects pages.layout
|
* Overrides BackendLayoutView so the page module always reflects pages.layout
|
||||||
* without the editor having to set backend_layout manually.
|
* 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
|
public function getSelectedCombinedIdentifier(int $pageId): string|false
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,60 +6,42 @@
|
|||||||
<sheets>
|
<sheets>
|
||||||
<sDEF>
|
<sDEF>
|
||||||
<ROOT>
|
<ROOT>
|
||||||
<TCEforms>
|
<sheetTitle>General Settings</sheetTitle>
|
||||||
<sheetTitle>General Settings</sheetTitle>
|
|
||||||
</TCEforms>
|
|
||||||
<type>array</type>
|
<type>array</type>
|
||||||
<el>
|
<el>
|
||||||
<settings.showFilter>
|
<settings.showFilter>
|
||||||
<TCEforms>
|
<label>Show Filter</label>
|
||||||
<label>Show Filter</label>
|
<config>
|
||||||
<config>
|
<type>check</type>
|
||||||
<type>check</type>
|
<default>1</default>
|
||||||
<default>1</default>
|
</config>
|
||||||
</config>
|
|
||||||
</TCEforms>
|
|
||||||
</settings.showFilter>
|
</settings.showFilter>
|
||||||
|
|
||||||
<settings.showSearch>
|
<settings.showSearch>
|
||||||
<TCEforms>
|
<label>Show Search</label>
|
||||||
<label>Show Search</label>
|
<config>
|
||||||
<config>
|
<type>check</type>
|
||||||
<type>check</type>
|
<default>1</default>
|
||||||
<default>1</default>
|
</config>
|
||||||
</config>
|
|
||||||
</TCEforms>
|
|
||||||
</settings.showSearch>
|
</settings.showSearch>
|
||||||
|
|
||||||
<settings.itemsPerPage>
|
<settings.itemsPerPage>
|
||||||
<TCEforms>
|
<label>Items per Page</label>
|
||||||
<label>Items per Page</label>
|
<config>
|
||||||
<config>
|
<type>number</type>
|
||||||
<type>number</type>
|
<default>20</default>
|
||||||
<default>20</default>
|
</config>
|
||||||
</config>
|
|
||||||
</TCEforms>
|
|
||||||
</settings.itemsPerPage>
|
</settings.itemsPerPage>
|
||||||
|
|
||||||
<settings.defaultCategory>
|
<settings.debug>
|
||||||
<TCEforms>
|
<label>Allow Debug Output</label>
|
||||||
<label>Default Category</label>
|
<config>
|
||||||
<config>
|
<type>check</type>
|
||||||
<type>select</type>
|
<default>0</default>
|
||||||
<renderType>selectSingle</renderType>
|
</config>
|
||||||
<foreign_table>tx_vitec_domain_model_downloadcategory</foreign_table>
|
</settings.debug>
|
||||||
<foreign_table_where>ORDER BY title</foreign_table_where>
|
|
||||||
<items>
|
|
||||||
<numIndex index="0">
|
|
||||||
<label>All Categories</label>
|
|
||||||
<value>0</value>
|
|
||||||
</numIndex>
|
|
||||||
</items>
|
|
||||||
</config>
|
|
||||||
</TCEforms>
|
|
||||||
</settings.defaultCategory>
|
|
||||||
</el>
|
</el>
|
||||||
</ROOT>
|
</ROOT>
|
||||||
</sDEF>
|
</sDEF>
|
||||||
</sheets>
|
</sheets>
|
||||||
</T3DataStructure>
|
</T3DataStructure>
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<T3DataStructure>
|
|
||||||
<sheets>
|
|
||||||
<sDEF>
|
|
||||||
<ROOT>
|
|
||||||
<sheetTitle>
|
|
||||||
Select Usecase
|
|
||||||
</sheetTitle>
|
|
||||||
<type>array</type>
|
|
||||||
<el>
|
|
||||||
<settings.usecase>
|
|
||||||
<label>
|
|
||||||
Usecase
|
|
||||||
</label>
|
|
||||||
<config>
|
|
||||||
<type>select</type>
|
|
||||||
<renderType>selectSingle</renderType>
|
|
||||||
<foreign_table>tx_vitec_domain_model_usecase</foreign_table>
|
|
||||||
<foreign_table_where>AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title</foreign_table_where>
|
|
||||||
<size>1</size>
|
|
||||||
<minitems>0</minitems>
|
|
||||||
<maxitems>1</maxitems>
|
|
||||||
</config>
|
|
||||||
</settings.usecase>
|
|
||||||
</el>
|
|
||||||
</ROOT>
|
|
||||||
</sDEF>
|
|
||||||
</sheets>
|
|
||||||
</T3DataStructure>
|
|
||||||
@@ -4,6 +4,46 @@ declare(strict_types=1);
|
|||||||
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
|
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
|
||||||
|
|
||||||
return [
|
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' => [
|
'vitec-cols-50-50' => [
|
||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-50-50.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-50-50.svg',
|
||||||
@@ -24,6 +64,10 @@ return [
|
|||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg',
|
'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' => [
|
'vitec-ogimage' => [
|
||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg',
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ settings:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- typo3/fluid-styled-content
|
- typo3/fluid-styled-content
|
||||||
- friendsoftypo3/headless
|
- friendsoftypo3/headless
|
||||||
- itplusx/headless-container
|
|
||||||
- nb-headless-content-blocks/headless-content-blocks
|
- nb-headless-content-blocks/headless-content-blocks
|
||||||
@@ -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.<list_type>` renders the element body for that subtype
|
# For every VITEC plugin the renderer's USER output is exposed as a sub-field
|
||||||
# (only ever invoked for the matching list_type — safe as-is).
|
# of the standard content-element envelope (`content.<key>`), inheriting all
|
||||||
#
|
# default fields (id, type, colPos, categories, appearance, …) from
|
||||||
# `tt_content.list.fields.content.fields.<key>` is GLOBAL: without scoping it
|
# lib.contentElement provided by friendsoftypo3/headless.
|
||||||
# would inject <key> 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 {
|
||||||
tt_content.list.20.vitec_productlist = USER
|
vitec_productlist < lib.contentElement
|
||||||
tt_content.list.20.vitec_productlist {
|
vitec_productlist {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
fields {
|
||||||
}
|
content {
|
||||||
|
fields {
|
||||||
tt_content.list.fields.content.fields.products = USER
|
products = USER
|
||||||
tt_content.list.fields.content.fields.products {
|
products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
}
|
||||||
stdWrap.if {
|
}
|
||||||
value = vitec_productlist
|
}
|
||||||
equals.field = list_type
|
|
||||||
}
|
}
|
||||||
ifEmptyUnsetKey = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- vitec_productshow --------------------------------------------------------
|
vitec_productshow < lib.contentElement
|
||||||
tt_content.list.20.vitec_productshow = USER
|
vitec_productshow {
|
||||||
tt_content.list.20.vitec_productshow {
|
fields {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
content {
|
||||||
}
|
fields {
|
||||||
|
product = USER
|
||||||
tt_content.list.fields.content.fields.product = USER
|
product.userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
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 --------------------------------------------------------
|
vitec_usecaselist < lib.contentElement
|
||||||
tt_content.list.20.vitec_usecaselist = USER
|
vitec_usecaselist {
|
||||||
tt_content.list.20.vitec_usecaselist {
|
fields {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
content {
|
||||||
}
|
fields {
|
||||||
|
usecases = USER
|
||||||
tt_content.list.fields.content.fields.usecases = USER
|
usecases.userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
||||||
tt_content.list.fields.content.fields.usecases {
|
}
|
||||||
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
}
|
||||||
stdWrap.if {
|
}
|
||||||
value = vitec_usecaselist
|
|
||||||
equals.field = list_type
|
|
||||||
}
|
}
|
||||||
ifEmptyUnsetKey = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- vitec_usecaseshow --------------------------------------------------------
|
vitec_usecaseshow < lib.contentElement
|
||||||
tt_content.list.20.vitec_usecaseshow = USER
|
vitec_usecaseshow {
|
||||||
tt_content.list.20.vitec_usecaseshow {
|
fields {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
content {
|
||||||
}
|
fields {
|
||||||
|
usecase = USER
|
||||||
tt_content.list.fields.content.fields.usecase = USER
|
usecase.userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
||||||
tt_content.list.fields.content.fields.usecase {
|
}
|
||||||
userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
}
|
||||||
stdWrap.if {
|
}
|
||||||
value = vitec_usecaseshow
|
}
|
||||||
equals.field = list_type
|
|
||||||
|
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
|
# Include container (layout) rendering definitions
|
||||||
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript'
|
@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
|
# Include menu (navigation) JSON definitions
|
||||||
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript'
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript'
|
||||||
@@ -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'
|
|
||||||
@@ -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.<list_type>` renders the element body for that subtype
|
|
||||||
# (only ever invoked for the matching list_type — safe as-is).
|
|
||||||
#
|
|
||||||
# `tt_content.list.fields.content.fields.<key>` is GLOBAL: without scoping it
|
|
||||||
# would inject <key> 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'
|
|
||||||
@@ -1,120 +1,53 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
defined('TYPO3') or die();
|
defined('TYPO3') or die();
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
|
||||||
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||||
|
|
||||||
// Register the Simple Card plugin
|
(static function (): void {
|
||||||
$simplecardPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
$registerPluginWithFlexForm = static function (
|
||||||
'Vitec',
|
string $pluginName,
|
||||||
'Simplecard',
|
string $pluginTitle,
|
||||||
'Simple Card'
|
string $flexFormFile,
|
||||||
);
|
?string $iconIdentifier = null
|
||||||
|
): void {
|
||||||
|
ExtensionUtility::registerPlugin(
|
||||||
|
'Vitec',
|
||||||
|
$pluginName,
|
||||||
|
$pluginTitle,
|
||||||
|
$iconIdentifier ?? 'content-plugin',
|
||||||
|
'plugins',
|
||||||
|
'',
|
||||||
|
$flexFormFile
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Register the "Single Success Story" plugin
|
$registerPluginWithFlexForm('Productlist', 'Show Products by Category', 'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml', 'vitec-plugin-productlist');
|
||||||
$usecaseShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
$registerPluginWithFlexForm('Simplecard', 'Simple Card', 'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml', 'vitec-plugin-simplecard');
|
||||||
'Vitec',
|
$registerPluginWithFlexForm('Productshow', 'Show Single Product', 'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml', 'vitec-plugin-productshow');
|
||||||
'Usecaseshow',
|
$registerPluginWithFlexForm('Usecaseshow', 'Single Success Story', 'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml', 'vitec-plugin-usecaseshow');
|
||||||
'Single Success Story'
|
$registerPluginWithFlexForm('Marketshow', 'Show Single Market', 'FILE:EXT:vitec/Configuration/FlexForms/Market.xml', 'vitec-plugin-marketshow');
|
||||||
);
|
$registerPluginWithFlexForm('Solutionshow', 'Show Single Solution', 'FILE:EXT:vitec/Configuration/FlexForms/Solution.xml', 'vitec-plugin-solutionshow');
|
||||||
|
$registerPluginWithFlexForm('Usecaselist', 'Shows List of all Success Stories', 'FILE:EXT:vitec/Configuration/FlexForms/Usecaselist.xml', 'vitec-plugin-usecaselist');
|
||||||
|
$registerPluginWithFlexForm('Downloadcard', 'Download Card', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcard.xml', 'vitec-plugin-downloadcard');
|
||||||
|
$registerPluginWithFlexForm('Downloadcardcollection', 'Download Card Collection', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml', 'vitec-plugin-downloadcardcollection');
|
||||||
|
$registerPluginWithFlexForm('Datasheets', 'Datasheets', 'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml');
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$usecaseShowPluginSignature] = 'pi_flexform';
|
// Change frame_class to allow multiple selections.
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
|
||||||
$usecaseShowPluginSignature,
|
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['maxitems'] = 999;
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register the "Show Single Market" plugin
|
// Add custom frame_class options for Vitec.
|
||||||
$marketShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'] = array_merge(
|
||||||
'Vitec',
|
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'],
|
||||||
'Marketshow',
|
[
|
||||||
'Show Single Market'
|
['label' => 'Vitec: Full Width', 'value' => 'vitec-full-width'],
|
||||||
);
|
['label' => 'Vitec: Centered Container', 'value' => 'vitec-centered'],
|
||||||
|
['label' => 'Vitec: Card Style', 'value' => 'vitec-card'],
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$marketShowPluginSignature] = 'pi_flexform';
|
['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'],
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'],
|
||||||
$marketShowPluginSignature,
|
]
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Market.xml'
|
);
|
||||||
);
|
})();
|
||||||
|
|
||||||
// Register the "Show Single Solution" plugin
|
|
||||||
$solutionShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
|
||||||
'Vitec',
|
|
||||||
'Solutionshow',
|
|
||||||
'Show Single Solution'
|
|
||||||
);
|
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$solutionShowPluginSignature] = 'pi_flexform';
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
$solutionShowPluginSignature,
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Solution.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register the "Show Products by Category" plugin
|
|
||||||
$productListPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
|
||||||
'Vitec',
|
|
||||||
'Productlist',
|
|
||||||
'Show Products by Category'
|
|
||||||
);
|
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$productListPluginSignature] = 'pi_flexform';
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
$productListPluginSignature,
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register the "Show Single Product" plugin
|
|
||||||
$productShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
|
||||||
'Vitec',
|
|
||||||
'Productshow',
|
|
||||||
'Show Single Product'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$productShowPluginSignature] = 'pi_flexform';
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
$productShowPluginSignature,
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register the "Shows List of all Success Stories" plugin
|
|
||||||
$usecaseListPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
|
|
||||||
'Vitec',
|
|
||||||
'Usecaselist',
|
|
||||||
'Shows List of all Success Stories'
|
|
||||||
);
|
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$usecaseListPluginSignature] = 'pi_flexform';
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
$usecaseListPluginSignature,
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Usecaselist.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$simplecardPluginSignature] = 'pi_flexform';
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
$simplecardPluginSignature,
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml'
|
|
||||||
);
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['vitec_datasheets'] = 'recursive,select_key,pages';
|
|
||||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_datasheets'] = 'pi_flexform';
|
|
||||||
ExtensionManagementUtility::addPiFlexFormValue(
|
|
||||||
'vitec_datasheets',
|
|
||||||
'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Change frame_class to allow multiple selections
|
|
||||||
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
|
|
||||||
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['maxitems'] = 999;
|
|
||||||
|
|
||||||
// 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'],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
@@ -47,7 +47,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
--palette--;;hidden,
|
--palette--;;hidden,
|
||||||
--palette--;;access';
|
--palette--;;access';
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_25_25_25_25 {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_25_25_25_25 {
|
||||||
iconIdentifier = vitec-cols-25-25-25-25
|
iconIdentifier = vitec-cols-25-25-25-25
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_25_25_25_25.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_25_25_25_25.title
|
||||||
@@ -56,5 +56,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_25_25_25_25
|
|||||||
CType = vitec_cols_25_25_25_25
|
CType = vitec_cols_25_25_25_25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -46,7 +46,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
--palette--;;hidden,
|
--palette--;;hidden,
|
||||||
--palette--;;access';
|
--palette--;;access';
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_33_33 {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_33_33 {
|
||||||
iconIdentifier = vitec-cols-33-33-33
|
iconIdentifier = vitec-cols-33-33-33
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_33_33.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_33_33.title
|
||||||
@@ -55,5 +55,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_33_33 {
|
|||||||
CType = vitec_cols_33_33_33
|
CType = vitec_cols_33_33_33
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
--palette--;;hidden,
|
--palette--;;hidden,
|
||||||
--palette--;;access';
|
--palette--;;access';
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_66 {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_66 {
|
||||||
iconIdentifier = vitec-cols-33-66
|
iconIdentifier = vitec-cols-33-66
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_66.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_66.title
|
||||||
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_66 {
|
|||||||
CType = vitec_cols_33_66
|
CType = vitec_cols_33_66
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
--palette--;;hidden,
|
--palette--;;hidden,
|
||||||
--palette--;;access';
|
--palette--;;access';
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_50_50 {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_50_50 {
|
||||||
iconIdentifier = vitec-cols-50-50
|
iconIdentifier = vitec-cols-50-50
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_50_50.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_50_50.title
|
||||||
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_50_50 {
|
|||||||
CType = vitec_cols_50_50
|
CType = vitec_cols_50_50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
--palette--;;hidden,
|
--palette--;;hidden,
|
||||||
--palette--;;access';
|
--palette--;;access';
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_66_33 {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_66_33 {
|
||||||
iconIdentifier = vitec-cols-66-33
|
iconIdentifier = vitec-cols-66-33
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_66_33.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_66_33.title
|
||||||
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_66_33 {
|
|||||||
CType = vitec_cols_66_33
|
CType = vitec_cols_66_33
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -56,7 +56,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
'vitec_container'
|
'vitec_container'
|
||||||
);
|
);
|
||||||
|
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
||||||
iconIdentifier = vitec-container
|
iconIdentifier = vitec-container
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
|
||||||
@@ -65,5 +65,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
|||||||
CType = vitec_container
|
CType = vitec_container
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
defined('TYPO3') or die();
|
|
||||||
|
|
||||||
use B13\Container\Tca\ContainerConfiguration;
|
|
||||||
use B13\Container\Tca\Registry;
|
|
||||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
|
|
||||||
(static function (): void {
|
|
||||||
$l = 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:';
|
|
||||||
|
|
||||||
/** @var Registry $registry */
|
|
||||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
|
||||||
|
|
||||||
$registry->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(<<<TSCONFIG
|
|
||||||
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
|
||||||
iconIdentifier = vitec-container
|
|
||||||
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
|
|
||||||
description = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.description
|
|
||||||
tt_content_defValues {
|
|
||||||
CType = vitec_container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TSCONFIG);
|
|
||||||
})();
|
|
||||||
@@ -28,10 +28,10 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Register VITEC wizard group header
|
// Register VITEC wizard group header
|
||||||
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
|
||||||
mod.wizards.newContentElement.wizardItems.vitec {
|
mod.wizards.newContentElement.wizardItems.vitec {
|
||||||
header = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header
|
header = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header
|
||||||
show = *
|
show = *
|
||||||
}
|
}
|
||||||
TSCONFIG);
|
TSCONFIG;
|
||||||
})();
|
})();
|
||||||
@@ -192,27 +192,27 @@ return [
|
|||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
],
|
],
|
||||||
\TYPO3\CMS\Core\Resource\File::FILETYPE_TEXT => [
|
\TYPO3\CMS\Core\Resource\FileType::TEXT->value => [
|
||||||
'showitem' => '
|
'showitem' => '
|
||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
],
|
],
|
||||||
\TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [
|
\TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [
|
||||||
'showitem' => '
|
'showitem' => '
|
||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
],
|
],
|
||||||
\TYPO3\CMS\Core\Resource\File::FILETYPE_AUDIO => [
|
\TYPO3\CMS\Core\Resource\FileType::AUDIO->value => [
|
||||||
'showitem' => '
|
'showitem' => '
|
||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
],
|
],
|
||||||
\TYPO3\CMS\Core\Resource\File::FILETYPE_VIDEO => [
|
\TYPO3\CMS\Core\Resource\FileType::VIDEO->value => [
|
||||||
'showitem' => '
|
'showitem' => '
|
||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
],
|
],
|
||||||
\TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [
|
\TYPO3\CMS\Core\Resource\FileType::APPLICATION->value => [
|
||||||
'showitem' => '
|
'showitem' => '
|
||||||
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
|
||||||
--palette--;;filePalette'
|
--palette--;;filePalette'
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
<?php
|
|
||||||
return [
|
|
||||||
'ctrl' => [
|
|
||||||
'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
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
/* ----------------------------------------------------- */
|
|
||||||
],
|
|
||||||
];
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
<?php
|
|
||||||
return [
|
|
||||||
'ctrl' => [
|
|
||||||
'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
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
/* ----------------------------------------------------- */
|
|
||||||
],
|
|
||||||
];
|
|
||||||
@@ -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
|
|
||||||
@@ -13,8 +13,7 @@ mod {
|
|||||||
title = List of VITEC Products
|
title = List of VITEC Products
|
||||||
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_productlist.description
|
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_productlist.description
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_productlist
|
||||||
list_type = vitec_productlist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
productshow {
|
productshow {
|
||||||
@@ -22,8 +21,7 @@ mod {
|
|||||||
title = Show single VITEC Product
|
title = Show single VITEC Product
|
||||||
description = Either show a single product coming from a list, OR select a specific product in flexform
|
description = Either show a single product coming from a list, OR select a specific product in flexform
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_productshow
|
||||||
list_type = vitec_productshow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
usecaseshow {
|
usecaseshow {
|
||||||
@@ -31,8 +29,7 @@ mod {
|
|||||||
title = Show single VITEC Success Story
|
title = Show single VITEC Success Story
|
||||||
description = Shows a selected Success Story in a Card with different layouts
|
description = Shows a selected Success Story in a Card with different layouts
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_usecaseshow
|
||||||
list_type = vitec_usecaseshow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
marketshow {
|
marketshow {
|
||||||
@@ -40,8 +37,7 @@ mod {
|
|||||||
title = Show single VITEC Market
|
title = Show single VITEC Market
|
||||||
description = Shows a selected Market in a Card with different layouts
|
description = Shows a selected Market in a Card with different layouts
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_marketshow
|
||||||
list_type = vitec_marketshow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
solutionshow {
|
solutionshow {
|
||||||
@@ -49,8 +45,7 @@ mod {
|
|||||||
title = Show single VITEC Solution
|
title = Show single VITEC Solution
|
||||||
description = Shows a selected Solution in a Card with different layouts
|
description = Shows a selected Solution in a Card with different layouts
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_solutionshow
|
||||||
list_type = vitec_solutionshow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
usecaselist {
|
usecaselist {
|
||||||
@@ -58,8 +53,7 @@ mod {
|
|||||||
title = Show all VITEC Success Stories
|
title = Show all VITEC Success Stories
|
||||||
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
|
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_usecaselist
|
||||||
list_type = vitec_usecaselist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloadcard {
|
downloadcard {
|
||||||
@@ -67,8 +61,7 @@ mod {
|
|||||||
title = Downloadcard for single Download
|
title = Downloadcard for single Download
|
||||||
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
|
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_downloadcard
|
||||||
list_type = vitec_downloadcard
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloadcardcollection {
|
downloadcardcollection {
|
||||||
@@ -76,8 +69,7 @@ mod {
|
|||||||
title = Downloadcard Collection
|
title = Downloadcard Collection
|
||||||
description = Multiple Downloads in one Card
|
description = Multiple Downloads in one Card
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_downloadcardcollection
|
||||||
list_type = vitec_downloadcardcollection
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
datasheets {
|
datasheets {
|
||||||
@@ -85,8 +77,7 @@ mod {
|
|||||||
title = VITEC Datasheets
|
title = VITEC Datasheets
|
||||||
description = Interactive datasheets with filtering and download functionality
|
description = Interactive datasheets with filtering and download functionality
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_datasheets
|
||||||
list_type = vitec_datasheets
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
simplecard {
|
simplecard {
|
||||||
@@ -94,8 +85,7 @@ mod {
|
|||||||
title = Simple Card
|
title = Simple Card
|
||||||
description = Simple Card (Image, Title, Text, Link)
|
description = Simple Card (Image, Title, Text, Link)
|
||||||
tt_content_defValues {
|
tt_content_defValues {
|
||||||
CType = list
|
CType = vitec_simplecard
|
||||||
list_type = vitec_simplecard
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||||
|
<rect x="1.5" y="2.5" width="13" height="11" rx="1.4" fill="none" stroke="#000" stroke-width="1.2"/>
|
||||||
|
<rect x="3" y="4" width="10" height="3.6" rx="0.4" fill="#000" opacity="0.55"/>
|
||||||
|
<rect x="3" y="8.3" width="7.5" height="1.2" rx="0.2" fill="#000" opacity="0.45"/>
|
||||||
|
<rect x="3" y="10" width="9" height="1" rx="0.2" fill="#000" opacity="0.3"/>
|
||||||
|
<rect x="3" y="11.4" width="4.2" height="1" rx="0.2" fill="#000" opacity="0.25"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
@@ -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
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff version="1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="messages">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="title">
|
||||||
|
<source>VITEC · Card</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="description">
|
||||||
|
<source>Multi-purpose card with image / icon, body, two CTAs, layout/background/aspect-ratio/alignment/border/shadow options.</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="tab_content.label">
|
||||||
|
<source>Content</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tab_layout.label">
|
||||||
|
<source>Layout</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tab_advanced.label">
|
||||||
|
<source>Advanced</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="eyebrow.label">
|
||||||
|
<source>Eyebrow (small pre-title)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image.label">
|
||||||
|
<source>Image</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image.description">
|
||||||
|
<source>Primary card image. Optional — leave empty for text-only / icon-only layouts.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="icon.label">
|
||||||
|
<source>Icon (SVG or PNG)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="icon.description">
|
||||||
|
<source>Small icon used as fallback when no image is set, or as a visual indicator in icon-focused layouts.</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="primary_cta.label">
|
||||||
|
<source>Primary CTA Link</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="primary_cta_label.label">
|
||||||
|
<source>Primary CTA Button Text</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="secondary_cta.label">
|
||||||
|
<source>Secondary CTA Link (optional)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="secondary_cta_label.label">
|
||||||
|
<source>Secondary CTA Button Text (optional)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="card_link.label">
|
||||||
|
<source>Whole-card link (optional)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="card_link.description">
|
||||||
|
<source>If set, the whole card becomes clickable. Overrides individual CTAs visually in the frontend.</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- layout_variant -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="layout_variant.label">
|
||||||
|
<source>Layout Variant</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.vertical.label">
|
||||||
|
<source>Vertical (image top, content below)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.horizontal.label">
|
||||||
|
<source>Horizontal (image left, content right)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.horizontal_reverse.label">
|
||||||
|
<source>Horizontal Reverse (content left, image right)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.image_overlay.label">
|
||||||
|
<source>Image Overlay (text over image)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.text_only.label">
|
||||||
|
<source>Text Only (no image)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.icon_card.label">
|
||||||
|
<source>Icon Card (icon focused, text below)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.feature.label">
|
||||||
|
<source>Feature (large highlighted card)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="layout_variant.items.compact.label">
|
||||||
|
<source>Compact (small card, tight spacing)</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- background_variant -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="background_variant.label">
|
||||||
|
<source>Background Variant</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.none.label">
|
||||||
|
<source>None</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.orange.label">
|
||||||
|
<source>Orange</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.blue.label">
|
||||||
|
<source>Blue</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.graphite.label">
|
||||||
|
<source>Graphite</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.midnight.label">
|
||||||
|
<source>Midnight</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.light.label">
|
||||||
|
<source>Light (neutral)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="background_variant.items.dark.label">
|
||||||
|
<source>Dark (neutral)</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- image_aspect_ratio -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="image_aspect_ratio.label">
|
||||||
|
<source>Image Aspect Ratio</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image_aspect_ratio.items.auto.label">
|
||||||
|
<source>Auto (original)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image_aspect_ratio.items.1_1.label">
|
||||||
|
<source>Square (1:1)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image_aspect_ratio.items.4_3.label">
|
||||||
|
<source>Landscape (4:3)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image_aspect_ratio.items.16_9.label">
|
||||||
|
<source>Widescreen (16:9)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="image_aspect_ratio.items.3_4.label">
|
||||||
|
<source>Portrait (3:4)</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- text_alignment -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="text_alignment.label">
|
||||||
|
<source>Text Alignment</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="text_alignment.items.left.label">
|
||||||
|
<source>Left</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="text_alignment.items.center.label">
|
||||||
|
<source>Center</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="text_alignment.items.right.label">
|
||||||
|
<source>Right</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- border_style -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="border_style.label">
|
||||||
|
<source>Border Style</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="border_style.items.none.label">
|
||||||
|
<source>None</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="border_style.items.soft.label">
|
||||||
|
<source>Soft (subtle line)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="border_style.items.hard.label">
|
||||||
|
<source>Hard (thick line)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="border_style.items.dashed.label">
|
||||||
|
<source>Dashed</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<!-- shadow -->
|
||||||
|
<!-- ============================================================== -->
|
||||||
|
<trans-unit id="shadow.label">
|
||||||
|
<source>Shadow</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="shadow.items.none.label">
|
||||||
|
<source>None</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="shadow.items.soft.label">
|
||||||
|
<source>Soft</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="shadow.items.medium.label">
|
||||||
|
<source>Medium</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="shadow.items.strong.label">
|
||||||
|
<source>Strong</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cssClass.label">
|
||||||
|
<source>Additional CSS Class</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cssClass.description">
|
||||||
|
<source>Custom CSS class rendered on the card root element in the frontend.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="debug.label">
|
||||||
|
<source>Allow Debug Output</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="debug.description">
|
||||||
|
<source>When enabled, extra debug fields may be included in the JSON output for this element.</source>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
<f:layout name="Preview"/>
|
||||||
|
<f:section name="Content">
|
||||||
|
|
||||||
|
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
|
||||||
|
|
||||||
|
<div class="vitec-preview vitec-preview--{data.background_variant}">
|
||||||
|
|
||||||
|
<f:comment>Thumbnail: image > icon > placeholder</f:comment>
|
||||||
|
<f:if condition="{data.image.0}">
|
||||||
|
<f:then>
|
||||||
|
<f:image image="{data.image.0}"
|
||||||
|
class="vitec-preview__thumb"
|
||||||
|
width="120c"
|
||||||
|
height="80c"
|
||||||
|
alt="Card image preview"/>
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<f:if condition="{data.icon.0}">
|
||||||
|
<f:then>
|
||||||
|
<f:image image="{data.icon.0}"
|
||||||
|
class="vitec-preview__thumb"
|
||||||
|
width="120c"
|
||||||
|
height="80"
|
||||||
|
alt="Card icon preview"/>
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<div class="vitec-preview__thumb-placeholder">
|
||||||
|
<f:switch expression="{data.layout_variant}">
|
||||||
|
<f:case value="text_only">📝</f:case>
|
||||||
|
<f:case value="icon_card">✦</f:case>
|
||||||
|
<f:case value="feature">⭐</f:case>
|
||||||
|
<f:case value="compact">▫</f:case>
|
||||||
|
<f:defaultCase>▢</f:defaultCase>
|
||||||
|
</f:switch>
|
||||||
|
</div>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<div class="vitec-preview__body">
|
||||||
|
<div class="vitec-preview__label">VITEC · Card</div>
|
||||||
|
|
||||||
|
<f:if condition="{data.eyebrow}">
|
||||||
|
<div class="vitec-preview__eyebrow">{data.eyebrow}</div>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<h3 class="vitec-preview__headline">
|
||||||
|
<f:if condition="{data.header}">
|
||||||
|
<f:then>{data.header}</f:then>
|
||||||
|
<f:else>
|
||||||
|
<em style="color:#c00;">⚠ Headline fehlt</em>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<f:if condition="{data.subheader}">
|
||||||
|
<div class="vitec-preview__subline">{data.subheader}</div>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.bodytext}">
|
||||||
|
<div class="vitec-preview__body-text">
|
||||||
|
<f:format.stripTags>{data.bodytext}</f:format.stripTags>
|
||||||
|
</div>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<div class="vitec-preview__ctas">
|
||||||
|
<f:if condition="{data.primary_cta_label}">
|
||||||
|
<span class="vitec-preview__cta-btn">{data.primary_cta_label} →</span>
|
||||||
|
</f:if>
|
||||||
|
<f:if condition="{data.secondary_cta_label}">
|
||||||
|
<span class="vitec-preview__cta-btn vitec-preview__cta-btn--secondary">
|
||||||
|
{data.secondary_cta_label}
|
||||||
|
</span>
|
||||||
|
</f:if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<f:comment>Settings sidebar with badges</f:comment>
|
||||||
|
<div class="vitec-preview__settings">
|
||||||
|
|
||||||
|
<span class="vitec-badge">
|
||||||
|
<f:switch expression="{data.layout_variant}">
|
||||||
|
<f:case value="vertical">▤ Vertical</f:case>
|
||||||
|
<f:case value="horizontal">▥ Horizontal</f:case>
|
||||||
|
<f:case value="horizontal_reverse">▤ Horizontal-Rev</f:case>
|
||||||
|
<f:case value="image_overlay">▣ Image Overlay</f:case>
|
||||||
|
<f:case value="text_only">📝 Text Only</f:case>
|
||||||
|
<f:case value="icon_card">✦ Icon Card</f:case>
|
||||||
|
<f:case value="feature">⭐ Feature</f:case>
|
||||||
|
<f:case value="compact">▫ Compact</f:case>
|
||||||
|
<f:defaultCase>{data.layout_variant}</f:defaultCase>
|
||||||
|
</f:switch>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="vitec-badge">
|
||||||
|
<span class="vitec-badge__dot vitec-badge__dot--{data.background_variant}"></span>
|
||||||
|
<f:switch expression="{data.background_variant}">
|
||||||
|
<f:case value="none">No BG</f:case>
|
||||||
|
<f:case value="orange">Orange</f:case>
|
||||||
|
<f:case value="blue">Blue</f:case>
|
||||||
|
<f:case value="graphite">Graphite</f:case>
|
||||||
|
<f:case value="midnight">Midnight</f:case>
|
||||||
|
<f:case value="light">Light</f:case>
|
||||||
|
<f:case value="dark">Dark</f:case>
|
||||||
|
<f:defaultCase>{data.background_variant}</f:defaultCase>
|
||||||
|
</f:switch>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<f:if condition="{data.image_aspect_ratio} != 'auto'">
|
||||||
|
<span class="vitec-badge">⛶ {data.image_aspect_ratio -> f:format.raw()}</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.text_alignment} != 'left'">
|
||||||
|
<span class="vitec-badge">
|
||||||
|
<f:switch expression="{data.text_alignment}">
|
||||||
|
<f:case value="center">↔ Center</f:case>
|
||||||
|
<f:case value="right">→ Right</f:case>
|
||||||
|
<f:defaultCase>{data.text_alignment}</f:defaultCase>
|
||||||
|
</f:switch>
|
||||||
|
</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.border_style} != 'none'">
|
||||||
|
<span class="vitec-badge">▭ Border: {data.border_style}</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.shadow} != 'none'">
|
||||||
|
<span class="vitec-badge">☁ Shadow: {data.shadow}</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.card_link.url}">
|
||||||
|
<span class="vitec-badge">🔗 Whole-card link</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.cssClass}">
|
||||||
|
<span class="vitec-badge">⌗ .{data.cssClass}</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{data.debug}">
|
||||||
|
<span class="vitec-badge vitec-badge--warning">⚙ Debug ON</span>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</f:section>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
|
||||||
|
</html>
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
|
||||||
<xliff version="1.0">
|
|
||||||
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
|
||||||
<header/>
|
|
||||||
<body>
|
|
||||||
<trans-unit id="group.header" resname="group.header">
|
|
||||||
<source>VITEC</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="section.heading" resname="section.heading">
|
|
||||||
<source>Section Heading</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="section.subline" resname="section.subline">
|
|
||||||
<source>Section Subline</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
|
||||||
<source>Background Variant</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
|
||||||
<source>None</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
|
||||||
<source>Orange</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
|
||||||
<source>Blue</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
|
||||||
<source>Graphite</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
|
||||||
<source>Midnight</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
|
||||||
<source>VITEC · Two Columns (50 / 50)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
|
||||||
<source>Two equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
|
||||||
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
|
||||||
<source>Three equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
|
||||||
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
|
||||||
<source>Four equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
|
||||||
<source>VITEC · Two Columns (66 / 33)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
|
||||||
<source>Wide main column + narrow sidebar</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
|
||||||
<source>VITEC · Two Columns (33 / 66)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
|
||||||
<source>Narrow sidebar + wide main column</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="column.1" resname="column.1">
|
|
||||||
<source>Column 1</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.2" resname="column.2">
|
|
||||||
<source>Column 2</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.3" resname="column.3">
|
|
||||||
<source>Column 3</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.4" resname="column.4">
|
|
||||||
<source>Column 4</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.main_66" resname="column.main_66">
|
|
||||||
<source>Main (66%)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
|
||||||
<source>Sidebar (33%)</source>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
|
||||||
<xliff version="1.0">
|
|
||||||
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
|
||||||
<header/>
|
|
||||||
<body>
|
|
||||||
<trans-unit id="group.header" resname="group.header">
|
|
||||||
<source>VITEC</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="section.heading" resname="section.heading">
|
|
||||||
<source>Section Heading</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="section.subline" resname="section.subline">
|
|
||||||
<source>Section Subline</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
|
||||||
<source>Background Variant</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
|
||||||
<source>None</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
|
||||||
<source>Orange</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
|
||||||
<source>Blue</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
|
||||||
<source>Graphite</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
|
||||||
<source>Midnight</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
|
||||||
<source>VITEC · Two Columns (50 / 50)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
|
||||||
<source>Two equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
|
||||||
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
|
||||||
<source>Three equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
|
||||||
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
|
||||||
<source>Four equal columns</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
|
||||||
<source>VITEC · Two Columns (66 / 33)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
|
||||||
<source>Wide main column + narrow sidebar</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
|
||||||
<source>VITEC · Two Columns (33 / 66)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
|
||||||
<source>Narrow sidebar + wide main column</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="container.title" resname="container.title">
|
|
||||||
<source>VITEC · Container</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="container.description" resname="container.description">
|
|
||||||
<source>Generic container for other content elements with a custom CSS class</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="container.column" resname="container.column">
|
|
||||||
<source>Content</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
|
|
||||||
<source>Container Settings</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
|
|
||||||
<source>CSS Class</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
|
|
||||||
<source>Custom CSS class that is rendered on the container in the frontend.</source>
|
|
||||||
</trans-unit>
|
|
||||||
|
|
||||||
<trans-unit id="column.1" resname="column.1">
|
|
||||||
<source>Column 1</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.2" resname="column.2">
|
|
||||||
<source>Column 2</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.3" resname="column.3">
|
|
||||||
<source>Column 3</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.4" resname="column.4">
|
|
||||||
<source>Column 4</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.main_66" resname="column.main_66">
|
|
||||||
<source>Main (66%)</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
|
||||||
<source>Sidebar (33%)</source>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
||||||
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 349 B |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 203 B |
|
Before Width: | Height: | Size: 202 B After Width: | Height: | Size: 202 B |
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 447 B |
@@ -6,7 +6,7 @@
|
|||||||
"authors": [],
|
"authors": [],
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"require": {
|
"require": {
|
||||||
"typo3/cms-core": "^13.4"
|
"typo3/cms-core": "^13.4 || ^14.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ $EM_CONF[$_EXTKEY] = [
|
|||||||
'version' => '1.0.1',
|
'version' => '1.0.1',
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
'depends' => [
|
'depends' => [
|
||||||
'typo3' => '13.4.0-13.4.99',
|
'typo3' => '13.4.0-14.9.99',
|
||||||
],
|
],
|
||||||
'conflicts' => [],
|
'conflicts' => [],
|
||||||
'suggests' => [],
|
'suggests' => [],
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
defined('TYPO3') || die();
|
defined('TYPO3') || die();
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
|
||||||
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||||
|
|
||||||
// Configure view template paths for the OG Image backend module
|
// Configure view template paths for the OG Image backend module
|
||||||
@@ -30,11 +28,6 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Backend\View\BackendLay
|
|||||||
// Register custom VITEC CKEditor RTE preset
|
// Register custom VITEC CKEditor RTE preset
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['vitec'] = 'EXT:vitec/Configuration/RTE/Vitec.yaml';
|
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['vitec'] = 'EXT:vitec/Configuration/RTE/Vitec.yaml';
|
||||||
|
|
||||||
// TSConfig für Seiten laden
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
|
|
||||||
"@import 'EXT:aifood/Configuration/page.tsconfig'"
|
|
||||||
);
|
|
||||||
|
|
||||||
(static function () {
|
(static function () {
|
||||||
// Register plugins
|
// Register plugins
|
||||||
ExtensionUtility::configurePlugin(
|
ExtensionUtility::configurePlugin(
|
||||||
@@ -146,52 +139,4 @@ $GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['vitec'] = 'EXT:vitec/Configuratio
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register icons
|
|
||||||
$iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
|
|
||||||
$iconRegistry->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']
|
|
||||||
);
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
@@ -11,48 +11,6 @@ ExtensionManagementUtility::addToAllTCAtypes(
|
|||||||
'categories'
|
'categories'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Include Page TSConfig
|
// Include Page TSConfig (TYPO3 v14 compatible registration)
|
||||||
ExtensionManagementUtility::addPageTSConfig(
|
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '')
|
||||||
'<INCLUDE_TYPOSCRIPT: source="FILE:EXT:vitec/Configuration/page.tsconfig">'
|
. "\n<INCLUDE_TYPOSCRIPT: source=\"FILE:EXT:vitec/Configuration/page.tsconfig\">";
|
||||||
);
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[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<e.length;u++)a=e[u],s=l+A(a,u),c+=ne(a,r,i,s,o);else if(u=m(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+A(a,u++),c+=ne(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ne(te(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function re(e,t,n){if(e==null)return e;var r=[],i=0;return ne(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function j(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t)},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var M=typeof reportError==`function`?reportError:function(e){if(typeof window==`object`&&typeof
Line truncated
|
||||||
|
at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return`
|
||||||
|
`+ve+e+ye}var xe=!1;function Se(e,t){if(!e||xe)return``;xe=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,`props`,{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}e.call(n.prototype)}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,`name`,{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
|
||||||
|
`),l=s.split(`
|
||||||
|
`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=`
|
||||||
|
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,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<n;){var u=31-He(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&rt(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function rt(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-He(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function it(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-He(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function at(e,t){var n=t&-t;return n=n&42?1:ot(n),(n&(e.suspendedLanes|t))===0?n:0}function ot(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;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:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function st(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function ct(){var e=N.p;return e===0?(e=window.event,e===void 0?32:gp(e.type)):e}function lt(e,t){var n=N.p;try{return N.p=e,t()}finally{N.p=n}}var ut=Math.random().toString(36).slice(2),dt=`__reactFiber$`+ut,ft=`__reactProps$`+ut,pt=`__reactContainer$`+ut,mt=`__reactEvents$`+ut,ht=`__reactListeners$`+ut,gt=`__reactHandles$`+ut,_t=`__reactResources$`+ut,vt=`__reactMarker$`+ut;function yt(e){delete e[dt],delete e[ft],delete e[mt],delete e[ht],delete e[gt]}function bt(e){var t=e[dt];if(t)return t;for(var n=e.parentNode;n;){if(t=n[pt]||n[dt]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=pf(e);e!==null;){if(n=e[dt])return n;e=pf(e)}return t}e=n,n=e.parentNode}return null}function xt(e){if(e=e[dt]||e[pt]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function St(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function Ct(e){var t=e[_t];return t||=e[_t]={hoistableStyles:new Map,hoistableScripts:new Map},t}function wt(e){e[vt]=!0}var Tt=new Set,Et={};function Dt(e,t){Ot(e,t),Ot(e+`Capture`,t)}function Ot(e,t){for(Et[e]=t,e=0;e<t.length;e++)Tt.add(t[e])}var kt=RegExp(`^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001Line truncated
|
||||||
|
`).replace(jd,``)}function Nd(e,t){return t=Md(t),Md(e)===t}function Pd(e,t,n,r,a,o){switch(n){case`children`:typeof r==`string`?t===`body`||t===`textarea`&&r===``||Xt(e,r):(typeof r==`number`||typeof r==`bigint`)&&t!==`body`&&Xt(e,``+r);break;case`className`:Pt(e,`class`,r);break;case`tabIndex`:Pt(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:Pt(e,n,r);break;case`style`:$t(e,r,o);break;case`data`:if(t!==`object`){Pt(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)){e.removeAttribute(n);break}if(r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=rn(``+r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}else typeof o==`function`&&(n===`formAction`?(t!==`input`&&Pd(e,t,`name`,a.name,a,null),Pd(e,t,`formEncType`,a.formEncType,a,null),Pd(e,t,`formMethod`,a.formMethod,a,null),Pd(e,t,`formTarget`,a.formTarget,a,null)):(Pd(e,t,`encType`,a.encType,a,null),Pd(e,t,`method`,a.method,a,null),Pd(e,t,`target`,a.target,a,null)));if(r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=rn(``+r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=an);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=rn(``+r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``+r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Z(`beforetoggle`,e),Z(`toggle`,e),Nt(e,`popover`,r);break;case`xlinkActuate`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:Ft(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:Ft(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:Ft(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:Ft(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:Nt(e,`is`,r);break;case`innerText`:case`textContent`:break;default:(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)&&(n=tn.get(n)||n,Nt(e,n,r))}}function Fd(e,t,n,r,a,o){switch(n){case`style`:$t(e,r,o);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`children`:typeof r==`string`?Xt(e,r):(typeof r==`number`||typeof r==`bigint`)&&Xt(e,``+r);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`onClick`:r!=null&&(e.onclick=an);break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`innerHTML`
Line truncated
|
||||||
|
|
||||||
|
Please change the parent <Route path="${e}"> to <Route path="${e===`/`?`*`:`${e}/*`}">.`)}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 \`<Routes location>\` 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 <Outlet /> 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<i.length;e++){let t=i[e];if((t.route.HydrateFallback||t.route.hydrateFallbackElement)&&(s=e),t.route.id){lLine truncated
|
||||||
|
`)),(Oc===void 0?Dc:kc)(l,a,o,s,c)}}Ac();function jc(e,t=-(2**53-1),n=2**53-1){return Math.max(t,Math.min(e,n))}var Mc=jc;function Nc(e,t=0,n=1){return Mc(e,t,n)}function Pc(e){e=e.slice(1);let t=RegExp(`.{1,${e.length>=6?2:1}}`,`g`),n=e.match(t);return n&&n[0].length===1&&(n=n.map(e=>e+e)),n?`rgb${n.length===4?`a`:``}(${n.map((e,t)=>t<3?parseInt(e,16):Math.round(parseInt(e,16)/255*1e3)/1e3).join(`, `)})`:``}function Fc(e){if(e.type)return e;if(e.charAt(0)===`#`)return Fc(Pc(e));let t=e.indexOf(`(`),n=e.substring(0,t);if(![`rgb`,`rgba`,`hsl`,`hsla`,`color`].includes(n))throw Error(oi(9,e));let r=e.substring(t+1,e.length-1),i;if(n===`color`){if(r=r.split(` `),i=r.shift(),r.length===4&&r[3].charAt(0)===`/`&&(r[3]=r[3].slice(1)),![`srgb`,`display-p3`,`a98-rgb`,`prophoto-rgb`,`rec-2020`].includes(i))throw Error(oi(10,i))}else r=r.split(`,`);return r=r.map(e=>parseFloat(e)),{type:n,values:r,colorSpace:i}}const Ic=e=>{let t=Fc(e);return t.values.slice(0,3).map((e,n)=>t.type.includes(`hsl`)&&n!==0?`${e}%`:e).join(` `)},Lc=(e,t)=>{try{return Ic(e)}catch{return e}};function Rc(e){let{type:t,colorSpace:n}=e,{values:r}=e;return t.includes(`rgb`)?r=r.map((e,t)=>t<3?parseInt(e,10):e):t.includes(`hsl`)&&(r[1]=`${r[1]}%`,r[2]=`${r[2]}%`),r=t.includes(`color`)?`${n} ${r.join(` `)}`:`${r.join(`, `)}`,`${t}(${r})`}function zc(e){e=Fc(e);let{values:t}=e,n=t[0],r=t[1]/100,i=t[2]/100,a=r*Math.min(i,1-i),o=(e,t=(e+n/30)%12)=>i-a*Math.max(Math.min(t-3,9-t,1),-1),s=`rgb`,c=[Math.round(o(0)*255),Math.round(o(8)*255),Math.round(o(4)*255)];return e.type===`hsla`&&(s+=`a`,c.push(t[3])),Rc({type:s,values:c})}function Bc(e){e=Fc(e);let t=e.type===`hsl`||e.type===`hsla`?Fc(zc(e)).values:e.values;return t=t.map(t=>(e.type!==`color`&&(t/=255),t<=.03928?t/12.92:((t+.055)/1.055)**2.4)),Number((.2126*t[0]+.7152*t[1]+.0722*t[2]).toFixed(3))}function Vc(e,t){let n=Bc(e),r=Bc(t);return(Math.max(n,r)+.05)/(Math.min(n,r)+.05)}function Hc(e,t){return e=Fc(e),t=Nc(t),(e.type===`rgb`||e.type===`hsl`)&&(e.type+=`a`),e.type===`color`?e.values[3]=`/${t}`:e.values[3]=t,Rc(e)}function Uc(e,t,n){try{return Hc(e,t)}catch{return e}}function Wc(e,t){if(e=Fc(e),t=Nc(t),e.type.includes(`hsl`))e.values[2]*=1-t;else if(e.type.includes(`rgb`)||e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]*=1-t;return Rc(e)}function V(e,t,n){try{return Wc(e,t)}catch{return e}}function Gc(e,t){if(e=Fc(e),t=Nc(t),e.type.includes(`hsl`))e.values[2]+=(100-e.values[2])*t;else if(e.type.includes(`rgb`))for(let n=0;n<3;n+=1)e.values[n]+=(255-e.values[n])*t;else if(e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]+=(1-e.values[n])*t;return Rc(e)}function H(e,t,n){try{return Gc(e,t)}catch{return e}}function Kc(e,t=.15){return Bc(e)>.5?Wc(e,t):Gc(e,t)}function qc(e,t,n){try{return Kc(e,t)}catch{return e}}var Jc=x.createContext();const Yc=()=>x.useContext(Jc)??!1;var Xc=x.createContext(void 0);function Zc(e){let{theme:t,name:n,props:r}=e;if(!t||!t.components||!t.components[n])return r;let i=t.components[n];return i.defaultProps?Cc(i.defaultProps,r,t.components.mergeClassNameAndStyle):!i.styleOverrides&&!i.variants?Cc(i,r,t.components.mergeClassNameAndStyle):r}function Qc({props:e,name:t}){return Zc({props:e,name:t,theme:{components:x.useContext(Xc)}})}var $c=0;function el(e){let[t,n]=x.useState(e),r=e||t;return x.useEffect(()=>{t??($c+=1,n(`mui-${$c}`))},[t]),r}var tl={...x}.useId;function nl(e){if(tl!==void 0){let t=tl();return e??t}return el(e)}var rl={theme:void 0};function il(e){let t,n;return function(r){let i=t;return(i===void 0||r.theme!==n)&&(rl.theme=r.theme,i=uc(e(rl)),t=i,n=r.theme),i}}function al(e=``){function t(...n){if(!n.length)return``;let r=n[0];return typeof r==`string`&&!r.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, var(--${e?`${e}-`:``}${r}${t(...n.slice(1))})`:`, ${r}`}return(n,...r)=>`var(--${e?`${e}-`:``}${n}${t(...r)})`}const ol=(e,t,n,r=[])=>{let i=e;t.forEach((e,a)=>{a===t.length-1?Array.isArray(i)?i[Number(e)]=n:i&&typeof i==`object`&&(i[e]=n):i&&typeof i==`object`&&(i[e]||(i[e]=r.includes(e)?[]:{}),i=i[e])})},sl=(e,t,n)=>{function r(e,i=[],a=[]){Object.entries(e).forEach(([e,o])=>{(!n||n&&!n([...i,e]))&&o!=null&&(typeof o==`object`&&Object.keys(o).length>0?r(o,[...i,e],Array.isArray(o)?[...a,e]:a):t([...i,e],o,a))})}r(e)};var cl=(e,t)=>typeof t==`number`?[`lineHeight`,`fontWeight`,`opacity`,`zIndex`].some(t=>e.includes(t))||e[e.length-1].toLowerCase().includes(`opacity`)?t:`${t}px`:t;function ll(e,t){let{prefix:n,shouldSkipGeneratingVar:r}=t||{},i={},a={},o={};return sl(e,(e,t,s)=>{if((typeof t==`string`||typeof t==`number`)&&(!r||!r(e,t))){let r=`--${n?`${n}-`:``}${e.join(`-`)}`,c=cl(e,t);Object.assign(i,{[r]:c}),ol(a,e,`var(${r})`,s),ol(o,e,`var(${r}, ${c})`,s)}},e=>e[0]===`vars`),{css:i,vars:a,varsWithDefaults:o}}function ul(e,t={}){let{getSelector:n=_,disableCssColorScheme:r,colorSchemeSelector:i,enableContrastVars:a}=t,{colorSchemes:o={},components:s,defaultColorS
Line truncated
|
||||||
|
|
||||||
|
const theme = ${JSON.stringify(t,null,2)};
|
||||||
|
|
||||||
|
theme.breakpoints = createBreakpoints(theme.breakpoints || {});
|
||||||
|
theme.transitions = createTransitions(theme.transitions || {});
|
||||||
|
|
||||||
|
export default theme;`}function wu(e){return typeof e==`number`?`${(e*100).toFixed(0)}%`:`calc((${e}) * 100%)`}var Tu=e=>{if(!Number.isNaN(+e))return+e;let t=e.match(/\d*\.?\d+/g);if(!t)return 0;let n=0;for(let e=0;e<t.length;e+=1)n+=+t[e];return n};function Eu(e){Object.assign(e,{alpha(t,n){let r=this||e;return r.colorSpace?`oklch(from ${t} l c h / ${typeof n==`string`?`calc(${n})`:n})`:r.vars?`rgba(${t.replace(/var\(--([^,\s)]+)(?:,[^)]+)?\)+/g,`var(--$1Channel)`)} / ${typeof n==`string`?`calc(${n})`:n})`:Hc(t,Tu(n))},lighten(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #fff ${wu(n)})`:Gc(t,n)},darken(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #000 ${wu(n)})`:Wc(t,n)}})}function Du(e={},...t){let{breakpoints:n,mixins:r={},spacing:i,palette:a={},transitions:o={},typography:s={},shape:c,colorSpace:l,...u}=e;if(e.vars&&e.generateThemeVars===void 0)throw Error(oi(20));let d=iu({...a,colorSpace:l}),f=qs(e),p=Co(f,{mixins:ou(f.breakpoints,r),palette:d,shadows:hu.slice(),typography:uu(d,s),transitions:bu(o),zIndex:{...xu}});return p=Co(p,u),p=t.reduce((e,t)=>Co(e,t),p),p.unstable_sxConfig={...zs,...u?.unstable_sxConfig},p.unstable_sx=function(e){return Ws({sx:e,theme:this})},p.toRuntimeSource=Cu,Eu(p),p}var Ou=Du;function ku(e){let t;return t=e<1?5.11916*e**2:4.5*Math.log(e+1)+2,Math.round(t*10)/1e3}var Au=[...Array(25)].map((e,t)=>{if(t===0)return`none`;let n=ku(t);return`linear-gradient(rgba(255 255 255 / ${n}), rgba(255 255 255 / ${n}))`});function ju(e){return{inputPlaceholder:e===`dark`?.5:.42,inputUnderline:e===`dark`?.7:.42,switchTrackDisabled:e===`dark`?.2:.12,switchTrack:e===`dark`?.3:.38}}function Mu(e){return e===`dark`?Au:[]}function Nu(e){let{palette:t={mode:`light`},opacity:n,overlays:r,colorSpace:i,...a}=e,o=iu({...t,colorSpace:i});return{palette:o,opacity:{...ju(o.mode),...n},overlays:r||Mu(o.mode),...a}}function Pu(e){return!!e[0].match(/(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/)||!!e[0].match(/sxConfig$/)||e[0]===`palette`&&!!e[1]?.match(/(mode|contrastThreshold|tonalOffset)/)}var Fu=e=>[...[...Array(25)].map((t,n)=>`--${e?`${e}-`:``}overlays-${n}`),`--${e?`${e}-`:``}palette-AppBar-darkBg`,`--${e?`${e}-`:``}palette-AppBar-darkColor`],Iu=e=>(t,n)=>{let r=e.rootSelector||`:root`,i=e.colorSchemeSelector,a=i;if(i===`class`&&(a=`.%s`),i===`data`&&(a=`[data-%s]`),i?.startsWith(`data-`)&&!i.includes(`%s`)&&(a=`[${i}="%s"]`),e.defaultColorScheme===t){if(t===`dark`){let i={};return Fu(e.cssVarPrefix).forEach(e=>{i[e]=n[e],delete n[e]}),a===`media`?{[r]:n,"@media (prefers-color-scheme: dark)":{[r]:i}}:a?{[a.replace(`%s`,t)]:i,[`${r}, ${a.replace(`%s`,t)}`]:n}:{[r]:{...n,...i}}}if(a&&a!==`media`)return`${r}, ${a.replace(`%s`,String(t))}`}else if(t){if(a===`media`)return{[`@media (prefers-color-scheme: ${String(t)})`]:{[r]:n}};if(a)return a.replace(`%s`,String(t))}return r};function Lu(e,t){t.forEach(t=>{e[t]||(e[t]={})})}function K(e,t,n){!e[t]&&n&&(e[t]=n)}function Ru(e){return typeof e!=`string`||!e.startsWith(`hsl`)?e:zc(e)}function zu(e,t){`${t}Channel`in e||(e[`${t}Channel`]=Lc(Ru(e[t]),`MUI: Can't create \`palette.${t}Channel\` because \`palette.${t}\` is not one of these formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().
|
||||||
|
To suppress this warning, you need to explicitly provide the \`palette.${t}Channel\` as a string (in rgb format, for example "12 12 12") or undefined if you want to remove the channel token.`))}function Bu(e){return typeof e==`number`?`${e}px`:typeof e==`string`||typeof e==`function`||Array.isArray(e)?e:`8px`}var Vu=e=>{try{return e()}catch{}};const Hu=(e=`mui`)=>al(e);function Uu(e,t,n,r,i){if(!n)return;n=n===!0?{}:n;let a=i===`dark`?`dark`:`light`;if(!r){t[i]=Nu({...n,palette:{mode:a,...n?.palette},colorSpace:e});return}let{palette:o,...s}=Ou({...r,palette:{mode:a,...n?.palette},colorSpace:e});return t[i]={...n,palette:o,opacity:{...ju(a),...n?.opacity},overlays:n?.overlays||Mu(a)},s}function Wu(e={},...t){let{colorSchemes:n={light:!0},defaultColorScheme:r,disableCssColorScheme:i=!1,cssVarPrefix:a=`mui`,nativeColor:o=!1,shouldSkipGeneratingVar:s=Pu,colorSchemeSelector:c=n.light&&n.dark?`media`:void 0,rootSelector:l=`:root`,...u}=e,d=Object.keys(n)[0],f=r||(n.light&&d!==`light`?`light`:d),p=Hu(a),{[f]:m,light:h,dark:g,..._}=n,v={..._},y=m;if((f===`dark`&&!(`dark`in n)||f===`light`&&!(`light`in n))&&(y=!0),!y)throw Error(oi(21,f));let b;o&&(b=`oklch`);let x=Uu(b,v,y,u,f);h&&!v.light&&Uu(b,v,h,void 0,`light`),g&&!v.dark&&Uu(b,v,g,void 0,`dark`);let S={defaultColorScheme:f,...x,cssVarPrefix:a,colorSchemeSelector:c,rootSelector:l,getCssVar:p,colorSchemes:v,font:{...au(x.typography),...x.font},spacing:Bu(u.spacing)};Object.keys(S.colorSchemes).forEach(e=>{let t=S.colorSchemes[e].palette,n=e=>{let n=e.split(`-`),r=n[1],i=n[2];return p(e,t[r][i])};t.mode===`light`&&(K(t.common,`background`,`#fff`),K(t.common,`onBackground`,`#000`)),t.mode===`dark`&&(K(t.common,`background`,`#000`),K(t.common,`onBackground`,`#fff`));function r(e,t,n){if(b){let r;return e===Uc&&(r=`transparent ${((1-n)*100).toFixed(0)}%`),e===V&&(r=`#000 ${(n*100).toFixed(0)}%`),e===H&&(r=`#fff ${(n*100).toFixed(0)}%`),`color-mix(in ${b}, ${t}, ${r})`}return e(t,n)}if(Lu(t,[`Alert`,`AppBar`,`Avatar`,`Button`,`Chip`,`FilledInput`,`LinearProgress`,`Skeleton`,`Slider`,`SnackbarContent`,`SpeedDialAction`,`StepConnector`,`StepContent`,`Switch`,`TableCell`,`Tooltip`]),t.mode===`light`){K(t.Alert,`errorColor`,r(V,t.error.light,.6)),K(t.Alert,`infoColor`,r(V,t.info.light,.6)),K(t.Alert,`successColor`,r(V,t.success.light,.6)),K(t.Alert,`warningColor`,r(V,t.warning.light,.6)),K(t.Alert,`errorFilledBg`,n(`palette-error-main`)),K(t.Alert,`infoFilledBg`,n(`palette-info-main`)),K(t.Alert,`successFilledBg`,n(`palette-success-main`)),K(t.Alert,`warningFilledBg`,n(`palette-warning-main`)),K(t.Alert,`errorFilledColor`,Vu(()=>t.getContrastText(t.error.main))),K(t.Alert,`infoFilledColor`,Vu(()=>t.getContrastText(t.info.main))),K(t.Alert,`successFilledColor`,Vu(()=>t.getContrastText(t.success.main))),K(t.Alert,`warningFilledColor`,Vu(()=>t.getContrastText(t.warning.main))),K(t.Alert,`errorStandardBg`,r(H,t.error.light,.9)),K(t.Alert,`infoStandardBg`,r(H,t.info.light,.9)),K(t.Alert,`successStandardBg`,r(H,t.success.light,.9)),K(t.Alert,`warningStandardBg`,r(H,t.warning.light,.9)),K(t.Alert,`errorIconColor`,n(`palette-error-main`)),K(t.Alert,`infoIconColor`,n(`palette-info-main`)),K(t.Alert,`successIconColor`,n(`palette-success-main`)),K(t.Alert,`warningIconColor`,n(`palette-warning-main`)),K(t.AppBar,`defaultBg`,n(`palette-grey-100`)),K(t.Avatar,`defaultBg`,n(`palette-grey-400`)),K(t.Button,`inheritContainedBg`,n(`palette-grey-300`)),K(t.Button,`inheritContainedHoverBg`,n(`palette-grey-A100`)),K(t.Chip,`defaultBorder`,n(`palette-grey-400`)),K(t.Chip,`defaultAvatarColor`,n(`palette-grey-700`)),K(t.Chip,`defaultIconColor`,n(`palette-grey-700`)),K(t.FilledInput,`bg`,`rgba(0, 0, 0, 0.06)`),K(t.FilledInput,`hoverBg`,`rgba(0, 0, 0, 0.09)`),K(t.FilledInput,`disabledBg`,`rgba(0, 0, 0, 0.12)`),K(t.LinearProgress,`primaryBg`,r(H,t.primary.main,.62)),K(t.LinearProgress,`secondaryBg`,r(H,t.secondary.main,.62)),K(t.LinearProgress,`errorBg`,r(H,t.error.main,.62)),K(t.LinearProgress,`infoBg`,r(H,t.info.main,.62)),K(t.LinearProgress,`successBg`,r(H,t.success.main,.62)),K(t.LinearProgress,`warningBg`,r(H,t.warning.main,.62)),K(t.Skeleton,`bg`,b?r(Uc,t.text.primary,.11):`rgba(${n(`palette-text-primaryChannel`)} / 0.11)`),K(t.Slider,`primaryTrack`,r(H,t.primary.main,.62)),K(t.Slider,`secondaryTrack`,r(H,t.secondary.main,.62)),K(t.Slider,`errorTrack`,r(H,t.error.main,.62)),K(t.Slider,`infoTrack`,r(H,t.info.main,.62)),K(t.Slider,`successTrack`,r(H,t.success.main,.62)),K(t.Slider,`warningTrack`,r(H,t.warning.main,.62));let e=b?r(V,t.background.default,.6825):qc(t.background.default,.8);K(t.SnackbarContent,`bg`,e),K(t.SnackbarContent,`color`,Vu(()=>b?Jl.text.primary:t.getContrastText(e))),K(t.SpeedDialAction,`fabHoverBg`,qc(t.background.paper,.15)),K(t.StepConnector,`border`,n(`palette-grey-400`)),K(t.StepContent,`border`,n(`palette-grey-400`)),K(t.Switch,`defaultColor`,n(`palette-common-white`)),K(t.Switch,`defaultDisabledColor`,n(`palette-grey-100`)),K(t.Switch,`primaryDisabledColor`,r(H,tLine truncated
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
`,kf=to`
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,Af=to`
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
`;const jf=q(`span`,{name:`MuiTouchRipple`,slot:`Root`})({overflow:`hidden`,pointerEvents:`none`,position:`absolute`,zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:`inherit`}),Mf=q(Tf,{name:`MuiTouchRipple`,slot:`Ripple`})`
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&.${Ef.rippleVisible} {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
animation-name: ${Of};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.${Ef.ripplePulsate} {
|
||||||
|
animation-duration: ${({theme:e})=>e.transitions.duration.shorter}ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.child} {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childLeaving} {
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: ${kf};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childPulsate} {
|
||||||
|
position: absolute;
|
||||||
|
/* @noflip */
|
||||||
|
left: 0px;
|
||||||
|
top: 0;
|
||||||
|
animation-name: ${Af};
|
||||||
|
animation-duration: 2500ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
`;var Nf=x.forwardRef(function(e,t){let{center:n=!1,classes:r={},className:i,...a}=X({props:e,name:`MuiTouchRipple`}),[o,s]=x.useState([]),c=x.useRef(0),l=x.useRef(null);x.useEffect(()=>{l.current&&=(l.current(),null)},[o]);let u=x.useRef(!1),d=$d(),f=x.useRef(null),p=x.useRef(null),m=x.useCallback(e=>{let{pulsate:t,rippleX:n,rippleY:i,rippleSize:a,cb:o}=e;s(e=>[...e,(0,B.jsx)(Mf,{classes:{ripple:z(r.ripple,Ef.ripple),rippleVisible:z(r.rippleVisible,Ef.rippleVisible),ripplePulsate:z(r.ripplePulsate,Ef.ripplePulsate),child:z(r.child,Ef.child),childLeaving:z(r.childLeaving,Ef.childLeaving),childPulsate:z(r.childPulsate,Ef.childPulsate)},timeout:Df,pulsate:t,rippleX:n,rippleY:i,rippleSize:a},c.current)]),c.current+=1,l.current=o},[r]),h=x.useCallback((e={},t={},r=()=>{})=>{let{pulsate:i=!1,center:a=n||t.pulsate,fakeElement:o=!1}=t;if(e?.type===`mousedown`&&u.current){u.current=!1;return}e?.type===`touchstart`&&(u.current=!0);let s=o?null:p.current,c=s?s.getBoundingClientRect():{width:0,height:0,left:0,top:0},l,h,g;if(a||e===void 0||e.clientX===0&&e.clientY===0||!e.clientX&&!e.touches)l=Math.round(c.width/2),h=Math.round(c.height/2);else{let{clientX:t,clientY:n}=e.touches&&e.touches.length>0?e.touches[0]:e;l=Math.round(t-c.left),h=Math.round(n-c.top)}if(a)g=Math.sqrt((2*c.width**2+c.height**2)/3),g%2==0&&(g+=1);else{let e=Math.max(Math.abs((s?s.clientWidth:0)-l),l)*2+2,t=Math.max(Math.abs((s?s.clientHeight:0)-h),h)*2+2;g=Math.sqrt(e**2+t**2)}e?.touches?f.current===null&&(f.current=()=>{m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},d.start(80,()=>{f.current&&=(f.current(),null)})):m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},[n,m,d]),g=x.useCallback(()=>{h({},{pulsate:!0})},[h]),_=x.useCallback((e,t)=>{if(d.clear(),e?.type===`touchend`&&f.current){f.current(),f.current=null,d.start(0,()=>{_(e,t)});return}f.current=null,s(e=>e.length>0?e.slice(1):e),l.current=t},[d]);return x.useImperativeHandle(t,()=>({pulsate:g,start:h,stop:_}),[g,h,_]),(0,B.jsx)(jf,{className:z(Ef.root,r.root,i),ref:p,...a,children:(0,B.jsx)(qd,{component:null,exit:!0,children:o})})});function Pf(e){return cc(`MuiButtonBase`,e)}var Ff=lc(`MuiButtonBase`,[`root`,`disabled`,`focusVisible`]),If=e=>{let{disabled:t,focusVisible:n,focusVisibleClassName:r,classes:i}=e,a=pl({root:[`root`,t&&`disabled`,n&&`focusVisible`]},Pf,i);return n&&r&&(a.root+=` ${r}`),a};const Lf=q(`button`,{name:`MuiButtonBase`,slot:`Root`})({display:`inline-flex`,alignItems:`center`,justifyContent:`center`,position:`relative`,boxSizing:`border-box`,WebkitTapHighlightColor:`transparent`,backgroundColor:`transparent`,outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:`pointer`,userSelect:`none`,verticalAlign:`middle`,MozAppearance:`none`,WebkitAppearance:`none`,textDecoration:`none`,color:`inherit`,"&::-moz-focus-inner":{borderStyle:`none`},[`&.${Ff.disabled}`]:{pointerEvents:`none`,cursor:`default`},"@media print":{colorAdjust:`exact`}});var Rf=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiButtonBase`}),{action:r,centerRipple:i=!1,children:a,className:o,component:s=`button`,disabled:c=!1,disableRipple:l=!1,disableTouchRipple:u=!1,focusRipple:d=!1,focusVisibleClassName:f,LinkComponent:p=`a`,onBlur:m,onClick:h,onContextMenu:g,onDragLeave:_,onFocus:v,onFocusVisible:y,onKeyDown:b,onKeyUp:S,onMouseDown:C,onMouseLeave:w,onMouseUp:T,onTouchEnd:E,onTouchMove:D,onTouchStart:O,tabIndex:ee=0,TouchRippleProps:k,touchRippleRef:A,type:te,...ne}=n,re=x.useRef(null),j=Sf(),M=Z(j.ref,A),[N,ie]=x.useState(!1);c&&N&&ie(!1),x.useImperativeHandle(r,()=>({focusVisible:()=>{ie(!0),re.current.focus()}}),[]);let ae=j.shouldMount&&!l&&!c;x.useEffect(()=>{N&&d&&!l&&j.pulsate()},[l,d,N,j]);let oe=zf(j,`start`,C,u),se=zf(j,`stop`,g,u),ce=zf(j,`stop`,_,u),le=zf(j,`stop`,T,u),ue=zf(j,`stop`,e=>{N&&e.preventDefault(),w&&w(e)},u),de=zf(j,`start`,O,u),fe=zf(j,`stop`,E,u),pe=zf(j,`stop`,D,u),me=zf(j,`stop`,e=>{bf(e.target)||ie(!1),m&&m(e)},!1),he=yd(e=>{re.current||=e.currentTarget,bf(e.target)&&(ie(!0),y&&y(e)),v&&v(e)}),ge=()=>{let e=re.current;return s&&s!==`button`&&!(e.tagName===`A`&&e.href)},_e=yd(e=>{d&&!e.repeat&&N&&e.key===` `&&j.stop(e,()=>{j.start(e)}),e.target===e.currentTarget&&ge()&&e.key===` `&&e.preventDefault(),b&&b(e),e.target===e.currentTarget&&ge()&&e.key===`Enter`&&!c&&(e.preventDefault(),h&&h(e))}),ve=yd(e=>{d&&e.key===` `&&N&&!e.defaultPrevented&&j.stop(e,()=>{j.pulsate(e)}),S&&S(e),h&&e.target===e.currentTarget&&ge()&&e.key===` `&&!e.defaultPrevented&&h(e)}),ye=s;ye===`button`&&(ne.href||ne.to)&&(ye=p);let be={};ye===`button`?(be.type=te===void 0?`button`:te,be.disabled=c):(!ne.href&&!ne.to&&(be.role=`button`),c&&(be[`aria-disabled`]=c));let xe=Z(t,re),Se={...n,centerRipple:i,component:s,disabled:c,disableRipple:l,disableTouchRipple:u,focusRipple:d,tabIndex:ee,focusVisible:N},Ce=If(Se);return(0,B.jsxs)(Lf,{as:ye,className:z(Ce.root,o),ownerState:Se,onBlur:me,onClick:h,onContextMenu:se,onFocus:he,onKeyDown:_e,onKeyUp:ve,onMouseDown:oe,onMouseLeave:ue,onMou
Line truncated
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`,Yf=to`
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 100px, 200px;
|
||||||
|
stroke-dashoffset: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: -126px;
|
||||||
|
}
|
||||||
|
`,Xf=typeof Jf==`string`?null:eo`
|
||||||
|
animation: ${Jf} 1.4s linear infinite;
|
||||||
|
`,Zf=typeof Yf==`string`?null:eo`
|
||||||
|
animation: ${Yf} 1.4s ease-in-out infinite;
|
||||||
|
`,Qf=e=>{let{classes:t,variant:n,color:r,disableShrink:i}=e;return pl({root:[`root`,n,`color${J(r)}`],svg:[`svg`],track:[`track`],circle:[`circle`,`circle${J(n)}`,i&&`circleDisableShrink`]},Kf,t)},$f=q(`span`,{name:`MuiCircularProgress`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,t[n.variant],t[`color${J(n.color)}`]]}})(Y(({theme:e})=>({display:`inline-block`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`transform`)}},{props:{variant:`indeterminate`},style:Xf||{animation:`${Jf} 1.4s linear infinite`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}}))]}))),ep=q(`svg`,{name:`MuiCircularProgress`,slot:`Svg`})({display:`block`}),tp=q(`circle`,{name:`MuiCircularProgress`,slot:`Circle`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.circle,t[`circle${J(n.variant)}`],n.disableShrink&&t.circleDisableShrink]}})(Y(({theme:e})=>({stroke:`currentColor`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`stroke-dashoffset`)}},{props:{variant:`indeterminate`},style:{strokeDasharray:`80px, 200px`,strokeDashoffset:0}},{props:({ownerState:e})=>e.variant===`indeterminate`&&!e.disableShrink,style:Zf||{animation:`${Yf} 1.4s ease-in-out infinite`}}]}))),np=q(`circle`,{name:`MuiCircularProgress`,slot:`Track`})(Y(({theme:e})=>({stroke:`currentColor`,opacity:(e.vars||e).palette.action.activatedOpacity}))),rp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiCircularProgress`}),{className:r,color:i=`primary`,disableShrink:a=!1,enableTrackSlot:o=!1,size:s=40,style:c,thickness:l=3.6,value:u=0,variant:d=`indeterminate`,...f}=n,p={...n,color:i,disableShrink:a,size:s,thickness:l,value:u,variant:d,enableTrackSlot:o},m=Qf(p),h={},g={},_={};if(d===`determinate`){let e=2*Math.PI*((qf-l)/2);h.strokeDasharray=e.toFixed(3),_[`aria-valuenow`]=Math.round(u),h.strokeDashoffset=`${((100-u)/100*e).toFixed(3)}px`,g.transform=`rotate(-90deg)`}return(0,B.jsx)($f,{className:z(m.root,r),style:{width:s,height:s,...g,...c},ownerState:p,ref:t,role:`progressbar`,..._,...f,children:(0,B.jsxs)(ep,{className:m.svg,ownerState:p,viewBox:`${qf/2} ${qf/2} ${qf} ${qf}`,children:[o?(0,B.jsx)(np,{className:m.track,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l,"aria-hidden":`true`}):null,(0,B.jsx)(tp,{className:m.circle,style:h,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l})]})})});function ip(e){return cc(`MuiIconButton`,e)}var ap=lc(`MuiIconButton`,[`root`,`disabled`,`colorInherit`,`colorPrimary`,`colorSecondary`,`colorError`,`colorInfo`,`colorSuccess`,`colorWarning`,`edgeStart`,`edgeEnd`,`sizeSmall`,`sizeMedium`,`sizeLarge`,`loading`,`loadingIndicator`,`loadingWrapper`]),op=e=>{let{classes:t,disabled:n,color:r,edge:i,size:a,loading:o}=e;return pl({root:[`root`,o&&`loading`,n&&`disabled`,r!==`default`&&`color${J(r)}`,i&&`edge${J(i)}`,`size${J(a)}`],loadingIndicator:[`loadingIndicator`],loadingWrapper:[`loadingWrapper`]},ip,t)},sp=q(Bf,{name:`MuiIconButton`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.loading&&t.loading,n.color!==`default`&&t[`color${J(n.color)}`],n.edge&&t[`edge${J(n.edge)}`],t[`size${J(n.size)}`]]}})(Y(({theme:e})=>({textAlign:`center`,flex:`0 0 auto`,fontSize:e.typography.pxToRem(24),padding:8,borderRadius:`50%`,color:(e.vars||e).palette.action.active,transition:e.transitions.create(`background-color`,{duration:e.transitions.duration.shortest}),variants:[{props:e=>!e.disableRipple,style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"&:hover":{backgroundColor:`var(--IconButton-hoverBg)`,"@media (hover: none)":{backgroundColor:`transparent`}}}},{props:{edge:`start`},style:{marginLeft:-12}},{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:{edge:`end`},style:{marginRight:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}}]})),Y(({theme:e})=>({variants:[{props:{color:`inherit`},style:{color:`inherit`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}})),...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity)}})),{props:{size:`small`},style:{padding:5,fontSize:e.typography.pxToRem(18)}},{props:{size:`large`},style:{padding:12,fontSize:e.typography.pxToRem(28)}}],[`&.${ap.disabled}`]:{backgroundColor:`transparent`,color:(e.vars||e).palette.action.disabled},[`&.${ap.loading}`]:{color:`transparent`}}))),cp=q(`span`,{name:`MuiIconButton`,slot:`LoadingIndicator`})(({theme:e})=>({display:`none`,position:`absolute`,visibility:`visible`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%)`,color:(e.vars||e).palette.action.disabled,variants:[{props:{loading:!0},style:{display:`flex`}}]})),lp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiIconButton`}),{ed
Line truncated
|
||||||
@@ -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;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[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<e.length;u++)a=e[u],s=l+A(a,u),c+=ne(a,r,i,s,o);else if(u=m(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+A(a,u++),c+=ne(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ne(te(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function re(e,t,n){if(e==null)return e;var r=[],i=0;return ne(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function j(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t)},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var M=typeof reportError==`function`?reportError:function(e){if(typeof window==`object`&&typeof
Line truncated
|
||||||
|
at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return`
|
||||||
|
`+_e+e+ve}var be=!1;function xe(e,t){if(!e||be)return``;be=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,`props`,{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}e.call(n.prototype)}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,`name`,{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
|
||||||
|
`),l=s.split(`
|
||||||
|
`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=`
|
||||||
|
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{be=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ye(n):``}function Se(e,t){switch(e.tag){case 26:case 27:case 5:return ye(e.type);case 16:return ye(`Lazy`);case 13:return e.child!==t&&t!==null?ye(`Suspense Fallback`):ye(`Suspense`);case 19:return ye(`SuspenseList`);case 0:case 15:return xe(e.type,!1);case 11:return xe(e.type.render,!1);case 1:return xe(e.type,!0);case 31:return ye(`Activity`);default:return``}}function Ce(e){try{var t=``,n=null;do t+=Se(e,n),n=e,e=e.return;while(e);return t}catch(e){return`
|
||||||
|
Error generating stack: `+e.message+`
|
||||||
|
`+e.stack}}var we=Object.prototype.hasOwnProperty,Te=t.unstable_scheduleCallback,Ee=t.unstable_cancelCallback,De=t.unstable_shouldYield,Oe=t.unstable_requestPaint,ke=t.unstable_now,Ae=t.unstable_getCurrentPriorityLevel,je=t.unstable_ImmediatePriority,Me=t.unstable_UserBlockingPriority,Ne=t.unstable_NormalPriority,Pe=t.unstable_LowPriority,Fe=t.unstable_IdlePriority,Ie=t.log,Le=t.unstable_setDisableYieldValue,Re=null,ze=null;function Be(e){if(typeof Ie==`function`&&Le(e),ze&&typeof ze.setStrictMode==`function`)try{ze.setStrictMode(Re,e)}catch{}}var Ve=Math.clz32?Math.clz32:We,He=Math.log,Ue=Math.LN2;function We(e){return e>>>=0,e===0?32:31-(He(e)/Ue|0)|0}var Ge=256,Ke=262144,qe=4194304;function Je(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 Ye(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=Je(n))):i=Je(o):i=Je(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Je(n))):i=Je(o)):i=Je(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 Xe(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ze(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 Qe(){var e=qe;return qe<<=1,!(qe&62914560)&&(qe=4194304),e}function $e(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function et(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function tt(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<n;){var u=31-Ve(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&nt(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function nt(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-Ve(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function rt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-Ve(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function it(e,t){var n=t&-t;return n=n&42?1:at(n),(n&(e.suspendedLanes|t))===0?n:0}function at(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;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:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function ot(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function st(){var e=N.p;return e===0?(e=window.event,e===void 0?32:gp(e.type)):e}function ct(e,t){var n=N.p;try{return N.p=e,t()}finally{N.p=n}}var lt=Math.random().toString(36).slice(2),ut=`__reactFiber$`+lt,dt=`__reactProps$`+lt,ft=`__reactContainer$`+lt,pt=`__reactEvents$`+lt,mt=`__reactListeners$`+lt,ht=`__reactHandles$`+lt,gt=`__reactResources$`+lt,_t=`__reactMarker$`+lt;function vt(e){delete e[ut],delete e[dt],delete e[pt],delete e[mt],delete e[ht]}function yt(e){var t=e[ut];if(t)return t;for(var n=e.parentNode;n;){if(t=n[ft]||n[ut]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=pf(e);e!==null;){if(n=e[ut])return n;e=pf(e)}return t}e=n,n=e.parentNode}return null}function bt(e){if(e=e[ut]||e[ft]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function xt(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function St(e){var t=e[gt];return t||=e[gt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function Ct(e){e[_t]=!0}var wt=new Set,Tt={};function Et(e,t){Dt(e,t),Dt(e+`Capture`,t)}function Dt(e,t){for(Tt[e]=t,e=0;e<t.length;e++)wt.add(t[e])}var Ot=RegExp(`^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001Line truncated
|
||||||
|
`).replace(jd,``)}function Nd(e,t){return t=Md(t),Md(e)===t}function Pd(e,t,n,r,a,o){switch(n){case`children`:typeof r==`string`?t===`body`||t===`textarea`&&r===``||Yt(e,r):(typeof r==`number`||typeof r==`bigint`)&&t!==`body`&&Yt(e,``+r);break;case`className`:Nt(e,`class`,r);break;case`tabIndex`:Nt(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:Nt(e,n,r);break;case`style`:Qt(e,r,o);break;case`data`:if(t!==`object`){Nt(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)){e.removeAttribute(n);break}if(r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=nn(``+r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}else typeof o==`function`&&(n===`formAction`?(t!==`input`&&Pd(e,t,`name`,a.name,a,null),Pd(e,t,`formEncType`,a.formEncType,a,null),Pd(e,t,`formMethod`,a.formMethod,a,null),Pd(e,t,`formTarget`,a.formTarget,a,null)):(Pd(e,t,`encType`,a.encType,a,null),Pd(e,t,`method`,a.method,a,null),Pd(e,t,`target`,a.target,a,null)));if(r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=nn(``+r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=rn);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=nn(``+r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``+r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Z(`beforetoggle`,e),Z(`toggle`,e),Mt(e,`popover`,r);break;case`xlinkActuate`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:Mt(e,`is`,r);break;case`innerText`:case`textContent`:break;default:(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)&&(n=en.get(n)||n,Mt(e,n,r))}}function Fd(e,t,n,r,a,o){switch(n){case`style`:Qt(e,r,o);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`children`:typeof r==`string`?Yt(e,r):(typeof r==`number`||typeof r==`bigint`)&&Yt(e,``+r);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`onClick`:r!=null&&(e.onclick=rn);break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`innerHTML`
Line truncated
|
||||||
|
|
||||||
|
Please change the parent <Route path="${e}"> to <Route path="${e===`/`?`*`:`${e}/*`}">.`)}let u=cn(),d;if(t){let e=typeof t==`string`?tt(t):t;Ye(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):it(e,{pathname:p});Xe(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Xe(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let h=xn(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Nt([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Nt([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Zt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function hn(){let e=An(),t=zt(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=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 gn=x.createElement(hn,null),_n=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=an(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Qt.Provider,{value:this.props.routeContext},x.createElement($t.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(yn,{error:e},t):t}};_n.contextType=Gt;var vn=new WeakMap;function yn({children:e,error:t}){let{basename:n}=x.useContext(Xt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=rn(t.digest);if(e){let r=vn.get(t);if(r)throw r;let i=Ht(e.location,n);if(Vt&&!vn.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vn.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function bn({routeContext:e,match:t,children:n}){let r=x.useContext(Ut);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Qt.Provider,{value:e},n)}function xn(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);Ye(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e<i.length;e++){let t=i[e];if((t.route.HydrateFallback||t.route.hydrateFallbackElement)&&(s=e),t.route.id){lLine truncated
|
||||||
|
`)),(Dc===void 0?Ec:Oc)(l,a,o,s,c)}}kc();function Ac(e,t=-(2**53-1),n=2**53-1){return Math.max(t,Math.min(e,n))}var jc=Ac;function Mc(e,t=0,n=1){return jc(e,t,n)}function Nc(e){e=e.slice(1);let t=RegExp(`.{1,${e.length>=6?2:1}}`,`g`),n=e.match(t);return n&&n[0].length===1&&(n=n.map(e=>e+e)),n?`rgb${n.length===4?`a`:``}(${n.map((e,t)=>t<3?parseInt(e,16):Math.round(parseInt(e,16)/255*1e3)/1e3).join(`, `)})`:``}function Pc(e){if(e.type)return e;if(e.charAt(0)===`#`)return Pc(Nc(e));let t=e.indexOf(`(`),n=e.substring(0,t);if(![`rgb`,`rgba`,`hsl`,`hsla`,`color`].includes(n))throw Error(ai(9,e));let r=e.substring(t+1,e.length-1),i;if(n===`color`){if(r=r.split(` `),i=r.shift(),r.length===4&&r[3].charAt(0)===`/`&&(r[3]=r[3].slice(1)),![`srgb`,`display-p3`,`a98-rgb`,`prophoto-rgb`,`rec-2020`].includes(i))throw Error(ai(10,i))}else r=r.split(`,`);return r=r.map(e=>parseFloat(e)),{type:n,values:r,colorSpace:i}}const Fc=e=>{let t=Pc(e);return t.values.slice(0,3).map((e,n)=>t.type.includes(`hsl`)&&n!==0?`${e}%`:e).join(` `)},Ic=(e,t)=>{try{return Fc(e)}catch{return e}};function Lc(e){let{type:t,colorSpace:n}=e,{values:r}=e;return t.includes(`rgb`)?r=r.map((e,t)=>t<3?parseInt(e,10):e):t.includes(`hsl`)&&(r[1]=`${r[1]}%`,r[2]=`${r[2]}%`),r=t.includes(`color`)?`${n} ${r.join(` `)}`:`${r.join(`, `)}`,`${t}(${r})`}function Rc(e){e=Pc(e);let{values:t}=e,n=t[0],r=t[1]/100,i=t[2]/100,a=r*Math.min(i,1-i),o=(e,t=(e+n/30)%12)=>i-a*Math.max(Math.min(t-3,9-t,1),-1),s=`rgb`,c=[Math.round(o(0)*255),Math.round(o(8)*255),Math.round(o(4)*255)];return e.type===`hsla`&&(s+=`a`,c.push(t[3])),Lc({type:s,values:c})}function zc(e){e=Pc(e);let t=e.type===`hsl`||e.type===`hsla`?Pc(Rc(e)).values:e.values;return t=t.map(t=>(e.type!==`color`&&(t/=255),t<=.03928?t/12.92:((t+.055)/1.055)**2.4)),Number((.2126*t[0]+.7152*t[1]+.0722*t[2]).toFixed(3))}function Bc(e,t){let n=zc(e),r=zc(t);return(Math.max(n,r)+.05)/(Math.min(n,r)+.05)}function Vc(e,t){return e=Pc(e),t=Mc(t),(e.type===`rgb`||e.type===`hsl`)&&(e.type+=`a`),e.type===`color`?e.values[3]=`/${t}`:e.values[3]=t,Lc(e)}function Hc(e,t,n){try{return Vc(e,t)}catch{return e}}function Uc(e,t){if(e=Pc(e),t=Mc(t),e.type.includes(`hsl`))e.values[2]*=1-t;else if(e.type.includes(`rgb`)||e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]*=1-t;return Lc(e)}function H(e,t,n){try{return Uc(e,t)}catch{return e}}function Wc(e,t){if(e=Pc(e),t=Mc(t),e.type.includes(`hsl`))e.values[2]+=(100-e.values[2])*t;else if(e.type.includes(`rgb`))for(let n=0;n<3;n+=1)e.values[n]+=(255-e.values[n])*t;else if(e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]+=(1-e.values[n])*t;return Lc(e)}function U(e,t,n){try{return Wc(e,t)}catch{return e}}function Gc(e,t=.15){return zc(e)>.5?Uc(e,t):Wc(e,t)}function Kc(e,t,n){try{return Gc(e,t)}catch{return e}}var qc=x.createContext();const Jc=()=>x.useContext(qc)??!1;var Yc=x.createContext(void 0);function Xc(e){let{theme:t,name:n,props:r}=e;if(!t||!t.components||!t.components[n])return r;let i=t.components[n];return i.defaultProps?Sc(i.defaultProps,r,t.components.mergeClassNameAndStyle):!i.styleOverrides&&!i.variants?Sc(i,r,t.components.mergeClassNameAndStyle):r}function Zc({props:e,name:t}){return Xc({props:e,name:t,theme:{components:x.useContext(Yc)}})}var Qc=0;function $c(e){let[t,n]=x.useState(e),r=e||t;return x.useEffect(()=>{t??(Qc+=1,n(`mui-${Qc}`))},[t]),r}var el={...x}.useId;function tl(e){if(el!==void 0){let t=el();return e??t}return $c(e)}var nl={theme:void 0};function rl(e){let t,n;return function(r){let i=t;return(i===void 0||r.theme!==n)&&(nl.theme=r.theme,i=lc(e(nl)),t=i,n=r.theme),i}}function il(e=``){function t(...n){if(!n.length)return``;let r=n[0];return typeof r==`string`&&!r.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, var(--${e?`${e}-`:``}${r}${t(...n.slice(1))})`:`, ${r}`}return(n,...r)=>`var(--${e?`${e}-`:``}${n}${t(...r)})`}const al=(e,t,n,r=[])=>{let i=e;t.forEach((e,a)=>{a===t.length-1?Array.isArray(i)?i[Number(e)]=n:i&&typeof i==`object`&&(i[e]=n):i&&typeof i==`object`&&(i[e]||(i[e]=r.includes(e)?[]:{}),i=i[e])})},ol=(e,t,n)=>{function r(e,i=[],a=[]){Object.entries(e).forEach(([e,o])=>{(!n||n&&!n([...i,e]))&&o!=null&&(typeof o==`object`&&Object.keys(o).length>0?r(o,[...i,e],Array.isArray(o)?[...a,e]:a):t([...i,e],o,a))})}r(e)};var sl=(e,t)=>typeof t==`number`?[`lineHeight`,`fontWeight`,`opacity`,`zIndex`].some(t=>e.includes(t))||e[e.length-1].toLowerCase().includes(`opacity`)?t:`${t}px`:t;function cl(e,t){let{prefix:n,shouldSkipGeneratingVar:r}=t||{},i={},a={},o={};return ol(e,(e,t,s)=>{if((typeof t==`string`||typeof t==`number`)&&(!r||!r(e,t))){let r=`--${n?`${n}-`:``}${e.join(`-`)}`,c=sl(e,t);Object.assign(i,{[r]:c}),al(a,e,`var(${r})`,s),al(o,e,`var(${r}, ${c})`,s)}},e=>e[0]===`vars`),{css:i,vars:a,varsWithDefaults:o}}function ll(e,t={}){let{getSelector:n=_,disableCssColorScheme:r,colorSchemeSelector:i,enableContrastVars:a}=t,{colorSchemes:o={},components:s,defaultColorS
Line truncated
|
||||||
|
|
||||||
|
const theme = ${JSON.stringify(t,null,2)};
|
||||||
|
|
||||||
|
theme.breakpoints = createBreakpoints(theme.breakpoints || {});
|
||||||
|
theme.transitions = createTransitions(theme.transitions || {});
|
||||||
|
|
||||||
|
export default theme;`}function Cu(e){return typeof e==`number`?`${(e*100).toFixed(0)}%`:`calc((${e}) * 100%)`}var wu=e=>{if(!Number.isNaN(+e))return+e;let t=e.match(/\d*\.?\d+/g);if(!t)return 0;let n=0;for(let e=0;e<t.length;e+=1)n+=+t[e];return n};function Tu(e){Object.assign(e,{alpha(t,n){let r=this||e;return r.colorSpace?`oklch(from ${t} l c h / ${typeof n==`string`?`calc(${n})`:n})`:r.vars?`rgba(${t.replace(/var\(--([^,\s)]+)(?:,[^)]+)?\)+/g,`var(--$1Channel)`)} / ${typeof n==`string`?`calc(${n})`:n})`:Vc(t,wu(n))},lighten(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #fff ${Cu(n)})`:Wc(t,n)},darken(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #000 ${Cu(n)})`:Uc(t,n)}})}function Eu(e={},...t){let{breakpoints:n,mixins:r={},spacing:i,palette:a={},transitions:o={},typography:s={},shape:c,colorSpace:l,...u}=e;if(e.vars&&e.generateThemeVars===void 0)throw Error(ai(20));let d=ru({...a,colorSpace:l}),f=Ks(e),p=So(f,{mixins:au(f.breakpoints,r),palette:d,shadows:mu.slice(),typography:lu(d,s),transitions:yu(o),zIndex:{...bu}});return p=So(p,u),p=t.reduce((e,t)=>So(e,t),p),p.unstable_sxConfig={...Rs,...u?.unstable_sxConfig},p.unstable_sx=function(e){return Us({sx:e,theme:this})},p.toRuntimeSource=Su,Tu(p),p}var Du=Eu;function Ou(e){let t;return t=e<1?5.11916*e**2:4.5*Math.log(e+1)+2,Math.round(t*10)/1e3}var ku=[...Array(25)].map((e,t)=>{if(t===0)return`none`;let n=Ou(t);return`linear-gradient(rgba(255 255 255 / ${n}), rgba(255 255 255 / ${n}))`});function Au(e){return{inputPlaceholder:e===`dark`?.5:.42,inputUnderline:e===`dark`?.7:.42,switchTrackDisabled:e===`dark`?.2:.12,switchTrack:e===`dark`?.3:.38}}function ju(e){return e===`dark`?ku:[]}function Mu(e){let{palette:t={mode:`light`},opacity:n,overlays:r,colorSpace:i,...a}=e,o=ru({...t,colorSpace:i});return{palette:o,opacity:{...Au(o.mode),...n},overlays:r||ju(o.mode),...a}}function Nu(e){return!!e[0].match(/(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/)||!!e[0].match(/sxConfig$/)||e[0]===`palette`&&!!e[1]?.match(/(mode|contrastThreshold|tonalOffset)/)}var Pu=e=>[...[...Array(25)].map((t,n)=>`--${e?`${e}-`:``}overlays-${n}`),`--${e?`${e}-`:``}palette-AppBar-darkBg`,`--${e?`${e}-`:``}palette-AppBar-darkColor`],Fu=e=>(t,n)=>{let r=e.rootSelector||`:root`,i=e.colorSchemeSelector,a=i;if(i===`class`&&(a=`.%s`),i===`data`&&(a=`[data-%s]`),i?.startsWith(`data-`)&&!i.includes(`%s`)&&(a=`[${i}="%s"]`),e.defaultColorScheme===t){if(t===`dark`){let i={};return Pu(e.cssVarPrefix).forEach(e=>{i[e]=n[e],delete n[e]}),a===`media`?{[r]:n,"@media (prefers-color-scheme: dark)":{[r]:i}}:a?{[a.replace(`%s`,t)]:i,[`${r}, ${a.replace(`%s`,t)}`]:n}:{[r]:{...n,...i}}}if(a&&a!==`media`)return`${r}, ${a.replace(`%s`,String(t))}`}else if(t){if(a===`media`)return{[`@media (prefers-color-scheme: ${String(t)})`]:{[r]:n}};if(a)return a.replace(`%s`,String(t))}return r};function Iu(e,t){t.forEach(t=>{e[t]||(e[t]={})})}function q(e,t,n){!e[t]&&n&&(e[t]=n)}function Lu(e){return typeof e!=`string`||!e.startsWith(`hsl`)?e:Rc(e)}function Ru(e,t){`${t}Channel`in e||(e[`${t}Channel`]=Ic(Lu(e[t]),`MUI: Can't create \`palette.${t}Channel\` because \`palette.${t}\` is not one of these formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().
|
||||||
|
To suppress this warning, you need to explicitly provide the \`palette.${t}Channel\` as a string (in rgb format, for example "12 12 12") or undefined if you want to remove the channel token.`))}function zu(e){return typeof e==`number`?`${e}px`:typeof e==`string`||typeof e==`function`||Array.isArray(e)?e:`8px`}var Bu=e=>{try{return e()}catch{}};const Vu=(e=`mui`)=>il(e);function Hu(e,t,n,r,i){if(!n)return;n=n===!0?{}:n;let a=i===`dark`?`dark`:`light`;if(!r){t[i]=Mu({...n,palette:{mode:a,...n?.palette},colorSpace:e});return}let{palette:o,...s}=Du({...r,palette:{mode:a,...n?.palette},colorSpace:e});return t[i]={...n,palette:o,opacity:{...Au(a),...n?.opacity},overlays:n?.overlays||ju(a)},s}function Uu(e={},...t){let{colorSchemes:n={light:!0},defaultColorScheme:r,disableCssColorScheme:i=!1,cssVarPrefix:a=`mui`,nativeColor:o=!1,shouldSkipGeneratingVar:s=Nu,colorSchemeSelector:c=n.light&&n.dark?`media`:void 0,rootSelector:l=`:root`,...u}=e,d=Object.keys(n)[0],f=r||(n.light&&d!==`light`?`light`:d),p=Vu(a),{[f]:m,light:h,dark:g,..._}=n,v={..._},y=m;if((f===`dark`&&!(`dark`in n)||f===`light`&&!(`light`in n))&&(y=!0),!y)throw Error(ai(21,f));let b;o&&(b=`oklch`);let x=Hu(b,v,y,u,f);h&&!v.light&&Hu(b,v,h,void 0,`light`),g&&!v.dark&&Hu(b,v,g,void 0,`dark`);let S={defaultColorScheme:f,...x,cssVarPrefix:a,colorSchemeSelector:c,rootSelector:l,getCssVar:p,colorSchemes:v,font:{...iu(x.typography),...x.font},spacing:zu(u.spacing)};Object.keys(S.colorSchemes).forEach(e=>{let t=S.colorSchemes[e].palette,n=e=>{let n=e.split(`-`),r=n[1],i=n[2];return p(e,t[r][i])};t.mode===`light`&&(q(t.common,`background`,`#fff`),q(t.common,`onBackground`,`#000`)),t.mode===`dark`&&(q(t.common,`background`,`#000`),q(t.common,`onBackground`,`#fff`));function r(e,t,n){if(b){let r;return e===Hc&&(r=`transparent ${((1-n)*100).toFixed(0)}%`),e===H&&(r=`#000 ${(n*100).toFixed(0)}%`),e===U&&(r=`#fff ${(n*100).toFixed(0)}%`),`color-mix(in ${b}, ${t}, ${r})`}return e(t,n)}if(Iu(t,[`Alert`,`AppBar`,`Avatar`,`Button`,`Chip`,`FilledInput`,`LinearProgress`,`Skeleton`,`Slider`,`SnackbarContent`,`SpeedDialAction`,`StepConnector`,`StepContent`,`Switch`,`TableCell`,`Tooltip`]),t.mode===`light`){q(t.Alert,`errorColor`,r(H,t.error.light,.6)),q(t.Alert,`infoColor`,r(H,t.info.light,.6)),q(t.Alert,`successColor`,r(H,t.success.light,.6)),q(t.Alert,`warningColor`,r(H,t.warning.light,.6)),q(t.Alert,`errorFilledBg`,n(`palette-error-main`)),q(t.Alert,`infoFilledBg`,n(`palette-info-main`)),q(t.Alert,`successFilledBg`,n(`palette-success-main`)),q(t.Alert,`warningFilledBg`,n(`palette-warning-main`)),q(t.Alert,`errorFilledColor`,Bu(()=>t.getContrastText(t.error.main))),q(t.Alert,`infoFilledColor`,Bu(()=>t.getContrastText(t.info.main))),q(t.Alert,`successFilledColor`,Bu(()=>t.getContrastText(t.success.main))),q(t.Alert,`warningFilledColor`,Bu(()=>t.getContrastText(t.warning.main))),q(t.Alert,`errorStandardBg`,r(U,t.error.light,.9)),q(t.Alert,`infoStandardBg`,r(U,t.info.light,.9)),q(t.Alert,`successStandardBg`,r(U,t.success.light,.9)),q(t.Alert,`warningStandardBg`,r(U,t.warning.light,.9)),q(t.Alert,`errorIconColor`,n(`palette-error-main`)),q(t.Alert,`infoIconColor`,n(`palette-info-main`)),q(t.Alert,`successIconColor`,n(`palette-success-main`)),q(t.Alert,`warningIconColor`,n(`palette-warning-main`)),q(t.AppBar,`defaultBg`,n(`palette-grey-100`)),q(t.Avatar,`defaultBg`,n(`palette-grey-400`)),q(t.Button,`inheritContainedBg`,n(`palette-grey-300`)),q(t.Button,`inheritContainedHoverBg`,n(`palette-grey-A100`)),q(t.Chip,`defaultBorder`,n(`palette-grey-400`)),q(t.Chip,`defaultAvatarColor`,n(`palette-grey-700`)),q(t.Chip,`defaultIconColor`,n(`palette-grey-700`)),q(t.FilledInput,`bg`,`rgba(0, 0, 0, 0.06)`),q(t.FilledInput,`hoverBg`,`rgba(0, 0, 0, 0.09)`),q(t.FilledInput,`disabledBg`,`rgba(0, 0, 0, 0.12)`),q(t.LinearProgress,`primaryBg`,r(U,t.primary.main,.62)),q(t.LinearProgress,`secondaryBg`,r(U,t.secondary.main,.62)),q(t.LinearProgress,`errorBg`,r(U,t.error.main,.62)),q(t.LinearProgress,`infoBg`,r(U,t.info.main,.62)),q(t.LinearProgress,`successBg`,r(U,t.success.main,.62)),q(t.LinearProgress,`warningBg`,r(U,t.warning.main,.62)),q(t.Skeleton,`bg`,b?r(Hc,t.text.primary,.11):`rgba(${n(`palette-text-primaryChannel`)} / 0.11)`),q(t.Slider,`primaryTrack`,r(U,t.primary.main,.62)),q(t.Slider,`secondaryTrack`,r(U,t.secondary.main,.62)),q(t.Slider,`errorTrack`,r(U,t.error.main,.62)),q(t.Slider,`infoTrack`,r(U,t.info.main,.62)),q(t.Slider,`successTrack`,r(U,t.success.main,.62)),q(t.Slider,`warningTrack`,r(U,t.warning.main,.62));let e=b?r(H,t.background.default,.6825):Kc(t.background.default,.8);q(t.SnackbarContent,`bg`,e),q(t.SnackbarContent,`color`,Bu(()=>b?ql.text.primary:t.getContrastText(e))),q(t.SpeedDialAction,`fabHoverBg`,Kc(t.background.paper,.15)),q(t.StepConnector,`border`,n(`palette-grey-400`)),q(t.StepContent,`border`,n(`palette-grey-400`)),q(t.Switch,`defaultColor`,n(`palette-common-white`)),q(t.Switch,`defaultDisabledColor`,n(`palette-grey-100`)),q(t.Switch,`primaryDisabledColor`,r(U,tLine truncated
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
`,kf=eo`
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,Af=eo`
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
`;const jf=J(`span`,{name:`MuiTouchRipple`,slot:`Root`})({overflow:`hidden`,pointerEvents:`none`,position:`absolute`,zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:`inherit`}),Mf=J(Tf,{name:`MuiTouchRipple`,slot:`Ripple`})`
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&.${Ef.rippleVisible} {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
animation-name: ${Of};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.${Ef.ripplePulsate} {
|
||||||
|
animation-duration: ${({theme:e})=>e.transitions.duration.shorter}ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.child} {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childLeaving} {
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: ${kf};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childPulsate} {
|
||||||
|
position: absolute;
|
||||||
|
/* @noflip */
|
||||||
|
left: 0px;
|
||||||
|
top: 0;
|
||||||
|
animation-name: ${Af};
|
||||||
|
animation-duration: 2500ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
`;var Nf=x.forwardRef(function(e,t){let{center:n=!1,classes:r={},className:i,...a}=X({props:e,name:`MuiTouchRipple`}),[o,s]=x.useState([]),c=x.useRef(0),l=x.useRef(null);x.useEffect(()=>{l.current&&=(l.current(),null)},[o]);let u=x.useRef(!1),d=$d(),f=x.useRef(null),p=x.useRef(null),m=x.useCallback(e=>{let{pulsate:t,rippleX:n,rippleY:i,rippleSize:a,cb:o}=e;s(e=>[...e,(0,V.jsx)(Mf,{classes:{ripple:B(r.ripple,Ef.ripple),rippleVisible:B(r.rippleVisible,Ef.rippleVisible),ripplePulsate:B(r.ripplePulsate,Ef.ripplePulsate),child:B(r.child,Ef.child),childLeaving:B(r.childLeaving,Ef.childLeaving),childPulsate:B(r.childPulsate,Ef.childPulsate)},timeout:Df,pulsate:t,rippleX:n,rippleY:i,rippleSize:a},c.current)]),c.current+=1,l.current=o},[r]),h=x.useCallback((e={},t={},r=()=>{})=>{let{pulsate:i=!1,center:a=n||t.pulsate,fakeElement:o=!1}=t;if(e?.type===`mousedown`&&u.current){u.current=!1;return}e?.type===`touchstart`&&(u.current=!0);let s=o?null:p.current,c=s?s.getBoundingClientRect():{width:0,height:0,left:0,top:0},l,h,g;if(a||e===void 0||e.clientX===0&&e.clientY===0||!e.clientX&&!e.touches)l=Math.round(c.width/2),h=Math.round(c.height/2);else{let{clientX:t,clientY:n}=e.touches&&e.touches.length>0?e.touches[0]:e;l=Math.round(t-c.left),h=Math.round(n-c.top)}if(a)g=Math.sqrt((2*c.width**2+c.height**2)/3),g%2==0&&(g+=1);else{let e=Math.max(Math.abs((s?s.clientWidth:0)-l),l)*2+2,t=Math.max(Math.abs((s?s.clientHeight:0)-h),h)*2+2;g=Math.sqrt(e**2+t**2)}e?.touches?f.current===null&&(f.current=()=>{m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},d.start(80,()=>{f.current&&=(f.current(),null)})):m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},[n,m,d]),g=x.useCallback(()=>{h({},{pulsate:!0})},[h]),_=x.useCallback((e,t)=>{if(d.clear(),e?.type===`touchend`&&f.current){f.current(),f.current=null,d.start(0,()=>{_(e,t)});return}f.current=null,s(e=>e.length>0?e.slice(1):e),l.current=t},[d]);return x.useImperativeHandle(t,()=>({pulsate:g,start:h,stop:_}),[g,h,_]),(0,V.jsx)(jf,{className:B(Ef.root,r.root,i),ref:p,...a,children:(0,V.jsx)(qd,{component:null,exit:!0,children:o})})});function Pf(e){return sc(`MuiButtonBase`,e)}var Ff=cc(`MuiButtonBase`,[`root`,`disabled`,`focusVisible`]),If=e=>{let{disabled:t,focusVisible:n,focusVisibleClassName:r,classes:i}=e,a=fl({root:[`root`,t&&`disabled`,n&&`focusVisible`]},Pf,i);return n&&r&&(a.root+=` ${r}`),a};const Lf=J(`button`,{name:`MuiButtonBase`,slot:`Root`})({display:`inline-flex`,alignItems:`center`,justifyContent:`center`,position:`relative`,boxSizing:`border-box`,WebkitTapHighlightColor:`transparent`,backgroundColor:`transparent`,outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:`pointer`,userSelect:`none`,verticalAlign:`middle`,MozAppearance:`none`,WebkitAppearance:`none`,textDecoration:`none`,color:`inherit`,"&::-moz-focus-inner":{borderStyle:`none`},[`&.${Ff.disabled}`]:{pointerEvents:`none`,cursor:`default`},"@media print":{colorAdjust:`exact`}});var Rf=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiButtonBase`}),{action:r,centerRipple:i=!1,children:a,className:o,component:s=`button`,disabled:c=!1,disableRipple:l=!1,disableTouchRipple:u=!1,focusRipple:d=!1,focusVisibleClassName:f,LinkComponent:p=`a`,onBlur:m,onClick:h,onContextMenu:g,onDragLeave:_,onFocus:v,onFocusVisible:y,onKeyDown:b,onKeyUp:S,onMouseDown:C,onMouseLeave:w,onMouseUp:T,onTouchEnd:E,onTouchMove:D,onTouchStart:O,tabIndex:ee=0,TouchRippleProps:k,touchRippleRef:A,type:te,...ne}=n,re=x.useRef(null),j=Sf(),M=Z(j.ref,A),[N,ie]=x.useState(!1);c&&N&&ie(!1),x.useImperativeHandle(r,()=>({focusVisible:()=>{ie(!0),re.current.focus()}}),[]);let ae=j.shouldMount&&!l&&!c;x.useEffect(()=>{N&&d&&!l&&j.pulsate()},[l,d,N,j]);let oe=zf(j,`start`,C,u),se=zf(j,`stop`,g,u),ce=zf(j,`stop`,_,u),P=zf(j,`stop`,T,u),le=zf(j,`stop`,e=>{N&&e.preventDefault(),w&&w(e)},u),ue=zf(j,`start`,O,u),de=zf(j,`stop`,E,u),fe=zf(j,`stop`,D,u),pe=zf(j,`stop`,e=>{bf(e.target)||ie(!1),m&&m(e)},!1),me=yd(e=>{re.current||=e.currentTarget,bf(e.target)&&(ie(!0),y&&y(e)),v&&v(e)}),he=()=>{let e=re.current;return s&&s!==`button`&&!(e.tagName===`A`&&e.href)},ge=yd(e=>{d&&!e.repeat&&N&&e.key===` `&&j.stop(e,()=>{j.start(e)}),e.target===e.currentTarget&&he()&&e.key===` `&&e.preventDefault(),b&&b(e),e.target===e.currentTarget&&he()&&e.key===`Enter`&&!c&&(e.preventDefault(),h&&h(e))}),_e=yd(e=>{d&&e.key===` `&&N&&!e.defaultPrevented&&j.stop(e,()=>{j.pulsate(e)}),S&&S(e),h&&e.target===e.currentTarget&&he()&&e.key===` `&&!e.defaultPrevented&&h(e)}),ve=s;ve===`button`&&(ne.href||ne.to)&&(ve=p);let ye={};ve===`button`?(ye.type=te===void 0?`button`:te,ye.disabled=c):(!ne.href&&!ne.to&&(ye.role=`button`),c&&(ye[`aria-disabled`]=c));let be=Z(t,re),xe={...n,centerRipple:i,component:s,disabled:c,disableRipple:l,disableTouchRipple:u,focusRipple:d,tabIndex:ee,focusVisible:N},Se=If(xe);return(0,V.jsxs)(Lf,{as:ve,className:B(Se.root,o),ownerState:xe,onBlur:pe,onClick:h,onContextMenu:se,onFocus:me,onKeyDown:ge,onKeyUp:_e,onMouseDown:oe,onMouseLeave:le,onMous
Line truncated
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`,Yf=eo`
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 100px, 200px;
|
||||||
|
stroke-dashoffset: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: -126px;
|
||||||
|
}
|
||||||
|
`,Xf=typeof Jf==`string`?null:$a`
|
||||||
|
animation: ${Jf} 1.4s linear infinite;
|
||||||
|
`,Zf=typeof Yf==`string`?null:$a`
|
||||||
|
animation: ${Yf} 1.4s ease-in-out infinite;
|
||||||
|
`,Qf=e=>{let{classes:t,variant:n,color:r,disableShrink:i}=e;return fl({root:[`root`,n,`color${Y(r)}`],svg:[`svg`],track:[`track`],circle:[`circle`,`circle${Y(n)}`,i&&`circleDisableShrink`]},Kf,t)},$f=J(`span`,{name:`MuiCircularProgress`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,t[n.variant],t[`color${Y(n.color)}`]]}})($u(({theme:e})=>({display:`inline-block`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`transform`)}},{props:{variant:`indeterminate`},style:Xf||{animation:`${Jf} 1.4s linear infinite`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}}))]}))),ep=J(`svg`,{name:`MuiCircularProgress`,slot:`Svg`})({display:`block`}),tp=J(`circle`,{name:`MuiCircularProgress`,slot:`Circle`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.circle,t[`circle${Y(n.variant)}`],n.disableShrink&&t.circleDisableShrink]}})($u(({theme:e})=>({stroke:`currentColor`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`stroke-dashoffset`)}},{props:{variant:`indeterminate`},style:{strokeDasharray:`80px, 200px`,strokeDashoffset:0}},{props:({ownerState:e})=>e.variant===`indeterminate`&&!e.disableShrink,style:Zf||{animation:`${Yf} 1.4s ease-in-out infinite`}}]}))),np=J(`circle`,{name:`MuiCircularProgress`,slot:`Track`})($u(({theme:e})=>({stroke:`currentColor`,opacity:(e.vars||e).palette.action.activatedOpacity}))),rp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiCircularProgress`}),{className:r,color:i=`primary`,disableShrink:a=!1,enableTrackSlot:o=!1,size:s=40,style:c,thickness:l=3.6,value:u=0,variant:d=`indeterminate`,...f}=n,p={...n,color:i,disableShrink:a,size:s,thickness:l,value:u,variant:d,enableTrackSlot:o},m=Qf(p),h={},g={},_={};if(d===`determinate`){let e=2*Math.PI*((qf-l)/2);h.strokeDasharray=e.toFixed(3),_[`aria-valuenow`]=Math.round(u),h.strokeDashoffset=`${((100-u)/100*e).toFixed(3)}px`,g.transform=`rotate(-90deg)`}return(0,V.jsx)($f,{className:B(m.root,r),style:{width:s,height:s,...g,...c},ownerState:p,ref:t,role:`progressbar`,..._,...f,children:(0,V.jsxs)(ep,{className:m.svg,ownerState:p,viewBox:`${qf/2} ${qf/2} ${qf} ${qf}`,children:[o?(0,V.jsx)(np,{className:m.track,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l,"aria-hidden":`true`}):null,(0,V.jsx)(tp,{className:m.circle,style:h,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l})]})})});function ip(e){return sc(`MuiIconButton`,e)}var ap=cc(`MuiIconButton`,[`root`,`disabled`,`colorInherit`,`colorPrimary`,`colorSecondary`,`colorError`,`colorInfo`,`colorSuccess`,`colorWarning`,`edgeStart`,`edgeEnd`,`sizeSmall`,`sizeMedium`,`sizeLarge`,`loading`,`loadingIndicator`,`loadingWrapper`]),op=e=>{let{classes:t,disabled:n,color:r,edge:i,size:a,loading:o}=e;return fl({root:[`root`,o&&`loading`,n&&`disabled`,r!==`default`&&`color${Y(r)}`,i&&`edge${Y(i)}`,`size${Y(a)}`],loadingIndicator:[`loadingIndicator`],loadingWrapper:[`loadingWrapper`]},ip,t)},sp=J(Bf,{name:`MuiIconButton`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.loading&&t.loading,n.color!==`default`&&t[`color${Y(n.color)}`],n.edge&&t[`edge${Y(n.edge)}`],t[`size${Y(n.size)}`]]}})($u(({theme:e})=>({textAlign:`center`,flex:`0 0 auto`,fontSize:e.typography.pxToRem(24),padding:8,borderRadius:`50%`,color:(e.vars||e).palette.action.active,transition:e.transitions.create(`background-color`,{duration:e.transitions.duration.shortest}),variants:[{props:e=>!e.disableRipple,style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"&:hover":{backgroundColor:`var(--IconButton-hoverBg)`,"@media (hover: none)":{backgroundColor:`transparent`}}}},{props:{edge:`start`},style:{marginLeft:-12}},{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:{edge:`end`},style:{marginRight:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}}]})),$u(({theme:e})=>({variants:[{props:{color:`inherit`},style:{color:`inherit`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}})),...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity)}})),{props:{size:`small`},style:{padding:5,fontSize:e.typography.pxToRem(18)}},{props:{size:`large`},style:{padding:12,fontSize:e.typography.pxToRem(28)}}],[`&.${ap.disabled}`]:{backgroundColor:`transparent`,color:(e.vars||e).palette.action.disabled},[`&.${ap.loading}`]:{color:`transparent`}}))),cp=J(`span`,{name:`MuiIconButton`,slot:`LoadingIndicator`})(({theme:e})=>({display:`none`,position:`absolute`,visibility:`visible`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%)`,color:(e.vars||e).palette.action.disabled,variants:[{props:{loading:!0},style:{display:`flex`}}]})),lp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiIconButton`}
Line truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;color:#ffffffde;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#242424;font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;font-weight:400;line-height:1.5}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}a{color:#646cff;-webkit-text-decoration:inherit;text-decoration:inherit;font-weight:500}a:hover{color:#535bf2}body{place-items:center;min-width:320px;min-height:100vh;margin:0;display:flex}h1{font-size:3.2em;line-height:1.1}button{cursor:pointer;background-color:#1a1a1a;border:1px solid #0000;border-radius:8px;padding:.6em 1.2em;font-family:inherit;font-size:1em;font-weight:500;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.vitec-element{margin-top:.5rem;margin-bottom:.5rem}.vitec-space-before--extra-small{margin-top:.5rem}.vitec-space-before--small{margin-top:1rem}.vitec-space-before--medium{margin-top:2.5rem}.vitec-space-before--large{margin-top:4rem}.vitec-space-before--extra-large{margin-top:6rem}.vitec-space-after--extra-small{margin-bottom:.5rem}.vitec-space-after--small{margin-bottom:1rem}.vitec-space-after--medium{margin-bottom:2.5rem}.vitec-space-after--large{margin-bottom:4rem}.vitec-space-after--extra-large{margin-bottom:6rem}*,:before,:after{box-sizing:border-box}body,html{color:#1f2937;background:#f4f7fb;width:100%;min-height:100vh;margin:0;font-family:Roboto,system-ui,-apple-system,sans-serif}#page{flex-direction:column;width:100%;min-height:100vh;display:flex}#header{color:#f8fafc;background:linear-gradient(120deg,#26358cbf 20%,#f4793680 100%);flex-direction:column;justify-content:center;gap:1rem;width:100%;min-height:300px;padding:1rem 1.75rem;display:flex;box-shadow:0 12px 30px #0f274947}.header-chrome{z-index:2;flex-direction:column;gap:1rem;display:flex;position:relative}#header.header--with-hero{min-height:0;box-shadow:none;background:0 0;justify-content:flex-start;gap:0;padding:0;position:relative;overflow:hidden}#header.header--with-hero .header-chrome{padding:1.65rem 1.75rem 0;position:absolute;inset:0 0 auto}.header-hero{color:#f8fafc;background:#0f172a;width:100%;min-height:620px;margin-left:0;position:relative;top:0;overflow:hidden}.header-hero-media,.header-hero-overlay{position:absolute;inset:0}.header-hero-media{opacity:.45;background-position:50%;background-repeat:no-repeat;background-size:cover}.header-hero-overlay{background:linear-gradient(120deg,#26358cc7 20%,#f4793694 100%)}.header-hero-content{z-index:1;align-items:center;min-height:620px;padding-top:9.5rem;padding-bottom:4rem;display:flex;position:relative}.header-hero-copy{line-height:1.6}.header-hero-copy p{margin:0 0 1rem}.header-hero-copy p:last-of-type{margin-bottom:0}.header-hero-copy a{color:inherit}.header-hero-copy mark{color:inherit;background:#facc15d9;padding:0 .2rem}.header-hero-image-sr{clip:rect(0,0,0,0);border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.header-top-row{justify-content:space-between;align-items:center;gap:1rem;width:100%;display:flex}.header-logo-link{align-items:center;display:inline-flex}.header-logo{object-fit:contain;width:auto;height:34px}.mobile-nav-toggle{color:#f8fafc;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:#0f172a33;border:1px solid #f8fafc2e;border-radius:999px;min-width:auto;padding:.6rem;display:none}.mobile-nav-toggle:hover{background:#0f172a52}.mobile-nav-toggle__icon{flex-direction:column;justify-content:space-between;width:1.3rem;height:1rem;display:inline-flex}.mobile-nav-toggle__icon span{transform-origin:50%;background:currentColor;border-radius:999px;width:100%;height:2px;transition:transform .2s,opacity .2s;display:block}.mobile-nav-toggle__icon--open span:first-child{transform:translateY(7px)rotate(45deg)}.mobile-nav-toggle__icon--open span:nth-child(2){opacity:0}.mobile-nav-toggle__icon--open span:nth-child(3){transform:translateY(-7px)rotate(-45deg)}.mobile-nav-drawer{color:#f8fafc;background:linear-gradient(#0f172a 0%,#1e3a8a 100%);border-left:1px solid #f8fafc1f;width:min(24rem,100vw)}.mobile-nav-drawer__inner{min-height:100%;padding:1.25rem}.mobile-nav-drawer__header{justify-content:space-between;align-items:center;gap:1rem;margin-bottom:1.25rem;display:flex}.mobile-nav-close{color:#f8fafc;border:1px solid #f8fafc24;border-radius:999px;min-width:auto;padding:.35rem .85rem}.mobile-nav-section-title{color:#e2e8f0b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:.6rem}.mobile-nav-item{border:1px solid #0000;border-radius:18px;margin-bottom:.35rem;padding:.75rem .9rem}.mobile-nav-item .MuiListItemText-primary{font-weight:600}.mobile-nav-item .MuiLLine truncated
|
||||||
@@ -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;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[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<e.length;u++)a=e[u],s=l+A(a,u),c+=ne(a,r,i,s,o);else if(u=m(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+A(a,u++),c+=ne(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ne(te(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function re(e,t,n){if(e==null)return e;var r=[],i=0;return ne(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function j(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t)},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var M=typeof reportError==`function`?reportError:function(e){if(typeof window==`object`&&typeof
Line truncated
|
||||||
|
at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return`
|
||||||
|
`+_e+e+ve}var be=!1;function xe(e,t){if(!e||be)return``;be=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,`props`,{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}e.call(n.prototype)}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,`name`,{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
|
||||||
|
`),l=s.split(`
|
||||||
|
`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=`
|
||||||
|
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{be=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ye(n):``}function Se(e,t){switch(e.tag){case 26:case 27:case 5:return ye(e.type);case 16:return ye(`Lazy`);case 13:return e.child!==t&&t!==null?ye(`Suspense Fallback`):ye(`Suspense`);case 19:return ye(`SuspenseList`);case 0:case 15:return xe(e.type,!1);case 11:return xe(e.type.render,!1);case 1:return xe(e.type,!0);case 31:return ye(`Activity`);default:return``}}function Ce(e){try{var t=``,n=null;do t+=Se(e,n),n=e,e=e.return;while(e);return t}catch(e){return`
|
||||||
|
Error generating stack: `+e.message+`
|
||||||
|
`+e.stack}}var we=Object.prototype.hasOwnProperty,Te=t.unstable_scheduleCallback,Ee=t.unstable_cancelCallback,De=t.unstable_shouldYield,Oe=t.unstable_requestPaint,ke=t.unstable_now,Ae=t.unstable_getCurrentPriorityLevel,je=t.unstable_ImmediatePriority,Me=t.unstable_UserBlockingPriority,Ne=t.unstable_NormalPriority,Pe=t.unstable_LowPriority,Fe=t.unstable_IdlePriority,Ie=t.log,Le=t.unstable_setDisableYieldValue,Re=null,ze=null;function Be(e){if(typeof Ie==`function`&&Le(e),ze&&typeof ze.setStrictMode==`function`)try{ze.setStrictMode(Re,e)}catch{}}var Ve=Math.clz32?Math.clz32:We,He=Math.log,Ue=Math.LN2;function We(e){return e>>>=0,e===0?32:31-(He(e)/Ue|0)|0}var Ge=256,Ke=262144,qe=4194304;function Je(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 Ye(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=Je(n))):i=Je(o):i=Je(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Je(n))):i=Je(o)):i=Je(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 Xe(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ze(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 Qe(){var e=qe;return qe<<=1,!(qe&62914560)&&(qe=4194304),e}function $e(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function et(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function tt(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<n;){var u=31-Ve(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&nt(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function nt(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-Ve(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function rt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-Ve(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function it(e,t){var n=t&-t;return n=n&42?1:at(n),(n&(e.suspendedLanes|t))===0?n:0}function at(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;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:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function ot(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function st(){var e=N.p;return e===0?(e=window.event,e===void 0?32:gp(e.type)):e}function ct(e,t){var n=N.p;try{return N.p=e,t()}finally{N.p=n}}var lt=Math.random().toString(36).slice(2),ut=`__reactFiber$`+lt,dt=`__reactProps$`+lt,ft=`__reactContainer$`+lt,pt=`__reactEvents$`+lt,mt=`__reactListeners$`+lt,ht=`__reactHandles$`+lt,gt=`__reactResources$`+lt,_t=`__reactMarker$`+lt;function vt(e){delete e[ut],delete e[dt],delete e[pt],delete e[mt],delete e[ht]}function yt(e){var t=e[ut];if(t)return t;for(var n=e.parentNode;n;){if(t=n[ft]||n[ut]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=pf(e);e!==null;){if(n=e[ut])return n;e=pf(e)}return t}e=n,n=e.parentNode}return null}function bt(e){if(e=e[ut]||e[ft]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function xt(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function St(e){var t=e[gt];return t||=e[gt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function Ct(e){e[_t]=!0}var wt=new Set,Tt={};function Et(e,t){Dt(e,t),Dt(e+`Capture`,t)}function Dt(e,t){for(Tt[e]=t,e=0;e<t.length;e++)wt.add(t[e])}var Ot=RegExp(`^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001Line truncated
|
||||||
|
`).replace(jd,``)}function Nd(e,t){return t=Md(t),Md(e)===t}function Pd(e,t,n,r,a,o){switch(n){case`children`:typeof r==`string`?t===`body`||t===`textarea`&&r===``||Yt(e,r):(typeof r==`number`||typeof r==`bigint`)&&t!==`body`&&Yt(e,``+r);break;case`className`:Nt(e,`class`,r);break;case`tabIndex`:Nt(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:Nt(e,n,r);break;case`style`:Qt(e,r,o);break;case`data`:if(t!==`object`){Nt(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)){e.removeAttribute(n);break}if(r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=nn(``+r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}else typeof o==`function`&&(n===`formAction`?(t!==`input`&&Pd(e,t,`name`,a.name,a,null),Pd(e,t,`formEncType`,a.formEncType,a,null),Pd(e,t,`formMethod`,a.formMethod,a,null),Pd(e,t,`formTarget`,a.formTarget,a,null)):(Pd(e,t,`encType`,a.encType,a,null),Pd(e,t,`method`,a.method,a,null),Pd(e,t,`target`,a.target,a,null)));if(r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=nn(``+r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=rn);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=nn(``+r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``+r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Z(`beforetoggle`,e),Z(`toggle`,e),Mt(e,`popover`,r);break;case`xlinkActuate`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:Pt(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:Pt(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:Mt(e,`is`,r);break;case`innerText`:case`textContent`:break;default:(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)&&(n=en.get(n)||n,Mt(e,n,r))}}function Fd(e,t,n,r,a,o){switch(n){case`style`:Qt(e,r,o);break;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));e.innerHTML=n}}break;case`children`:typeof r==`string`?Yt(e,r):(typeof r==`number`||typeof r==`bigint`)&&Yt(e,``+r);break;case`onScroll`:r!=null&&Z(`scroll`,e);break;case`onScrollEnd`:r!=null&&Z(`scrollend`,e);break;case`onClick`:r!=null&&(e.onclick=rn);break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`innerHTML`
Line truncated
|
||||||
|
|
||||||
|
Please change the parent <Route path="${e}"> to <Route path="${e===`/`?`*`:`${e}/*`}">.`)}let u=cn(),d;if(t){let e=typeof t==`string`?tt(t):t;Ye(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):it(e,{pathname:p});Xe(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Xe(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let h=xn(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Nt([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Nt([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Zt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function hn(){let e=An(),t=zt(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=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 gn=x.createElement(hn,null),_n=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=an(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Qt.Provider,{value:this.props.routeContext},x.createElement($t.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(yn,{error:e},t):t}};_n.contextType=Gt;var vn=new WeakMap;function yn({children:e,error:t}){let{basename:n}=x.useContext(Xt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=rn(t.digest);if(e){let r=vn.get(t);if(r)throw r;let i=Ht(e.location,n);if(Vt&&!vn.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vn.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function bn({routeContext:e,match:t,children:n}){let r=x.useContext(Ut);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Qt.Provider,{value:e},n)}function xn(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);Ye(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e<i.length;e++){let t=i[e];if((t.route.HydrateFallback||t.route.hydrateFallbackElement)&&(s=e),t.route.id){lLine truncated
|
||||||
|
`)),(Dc===void 0?Ec:Oc)(l,a,o,s,c)}}kc();function Ac(e,t=-(2**53-1),n=2**53-1){return Math.max(t,Math.min(e,n))}var jc=Ac;function Mc(e,t=0,n=1){return jc(e,t,n)}function Nc(e){e=e.slice(1);let t=RegExp(`.{1,${e.length>=6?2:1}}`,`g`),n=e.match(t);return n&&n[0].length===1&&(n=n.map(e=>e+e)),n?`rgb${n.length===4?`a`:``}(${n.map((e,t)=>t<3?parseInt(e,16):Math.round(parseInt(e,16)/255*1e3)/1e3).join(`, `)})`:``}function Pc(e){if(e.type)return e;if(e.charAt(0)===`#`)return Pc(Nc(e));let t=e.indexOf(`(`),n=e.substring(0,t);if(![`rgb`,`rgba`,`hsl`,`hsla`,`color`].includes(n))throw Error(ai(9,e));let r=e.substring(t+1,e.length-1),i;if(n===`color`){if(r=r.split(` `),i=r.shift(),r.length===4&&r[3].charAt(0)===`/`&&(r[3]=r[3].slice(1)),![`srgb`,`display-p3`,`a98-rgb`,`prophoto-rgb`,`rec-2020`].includes(i))throw Error(ai(10,i))}else r=r.split(`,`);return r=r.map(e=>parseFloat(e)),{type:n,values:r,colorSpace:i}}const Fc=e=>{let t=Pc(e);return t.values.slice(0,3).map((e,n)=>t.type.includes(`hsl`)&&n!==0?`${e}%`:e).join(` `)},Ic=(e,t)=>{try{return Fc(e)}catch{return e}};function Lc(e){let{type:t,colorSpace:n}=e,{values:r}=e;return t.includes(`rgb`)?r=r.map((e,t)=>t<3?parseInt(e,10):e):t.includes(`hsl`)&&(r[1]=`${r[1]}%`,r[2]=`${r[2]}%`),r=t.includes(`color`)?`${n} ${r.join(` `)}`:`${r.join(`, `)}`,`${t}(${r})`}function Rc(e){e=Pc(e);let{values:t}=e,n=t[0],r=t[1]/100,i=t[2]/100,a=r*Math.min(i,1-i),o=(e,t=(e+n/30)%12)=>i-a*Math.max(Math.min(t-3,9-t,1),-1),s=`rgb`,c=[Math.round(o(0)*255),Math.round(o(8)*255),Math.round(o(4)*255)];return e.type===`hsla`&&(s+=`a`,c.push(t[3])),Lc({type:s,values:c})}function zc(e){e=Pc(e);let t=e.type===`hsl`||e.type===`hsla`?Pc(Rc(e)).values:e.values;return t=t.map(t=>(e.type!==`color`&&(t/=255),t<=.03928?t/12.92:((t+.055)/1.055)**2.4)),Number((.2126*t[0]+.7152*t[1]+.0722*t[2]).toFixed(3))}function Bc(e,t){let n=zc(e),r=zc(t);return(Math.max(n,r)+.05)/(Math.min(n,r)+.05)}function Vc(e,t){return e=Pc(e),t=Mc(t),(e.type===`rgb`||e.type===`hsl`)&&(e.type+=`a`),e.type===`color`?e.values[3]=`/${t}`:e.values[3]=t,Lc(e)}function Hc(e,t,n){try{return Vc(e,t)}catch{return e}}function Uc(e,t){if(e=Pc(e),t=Mc(t),e.type.includes(`hsl`))e.values[2]*=1-t;else if(e.type.includes(`rgb`)||e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]*=1-t;return Lc(e)}function H(e,t,n){try{return Uc(e,t)}catch{return e}}function Wc(e,t){if(e=Pc(e),t=Mc(t),e.type.includes(`hsl`))e.values[2]+=(100-e.values[2])*t;else if(e.type.includes(`rgb`))for(let n=0;n<3;n+=1)e.values[n]+=(255-e.values[n])*t;else if(e.type.includes(`color`))for(let n=0;n<3;n+=1)e.values[n]+=(1-e.values[n])*t;return Lc(e)}function U(e,t,n){try{return Wc(e,t)}catch{return e}}function Gc(e,t=.15){return zc(e)>.5?Uc(e,t):Wc(e,t)}function Kc(e,t,n){try{return Gc(e,t)}catch{return e}}var qc=x.createContext();const Jc=()=>x.useContext(qc)??!1;var Yc=x.createContext(void 0);function Xc(e){let{theme:t,name:n,props:r}=e;if(!t||!t.components||!t.components[n])return r;let i=t.components[n];return i.defaultProps?Sc(i.defaultProps,r,t.components.mergeClassNameAndStyle):!i.styleOverrides&&!i.variants?Sc(i,r,t.components.mergeClassNameAndStyle):r}function Zc({props:e,name:t}){return Xc({props:e,name:t,theme:{components:x.useContext(Yc)}})}var Qc=0;function $c(e){let[t,n]=x.useState(e),r=e||t;return x.useEffect(()=>{t??(Qc+=1,n(`mui-${Qc}`))},[t]),r}var el={...x}.useId;function tl(e){if(el!==void 0){let t=el();return e??t}return $c(e)}var nl={theme:void 0};function rl(e){let t,n;return function(r){let i=t;return(i===void 0||r.theme!==n)&&(nl.theme=r.theme,i=lc(e(nl)),t=i,n=r.theme),i}}function il(e=``){function t(...n){if(!n.length)return``;let r=n[0];return typeof r==`string`&&!r.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, var(--${e?`${e}-`:``}${r}${t(...n.slice(1))})`:`, ${r}`}return(n,...r)=>`var(--${e?`${e}-`:``}${n}${t(...r)})`}const al=(e,t,n,r=[])=>{let i=e;t.forEach((e,a)=>{a===t.length-1?Array.isArray(i)?i[Number(e)]=n:i&&typeof i==`object`&&(i[e]=n):i&&typeof i==`object`&&(i[e]||(i[e]=r.includes(e)?[]:{}),i=i[e])})},ol=(e,t,n)=>{function r(e,i=[],a=[]){Object.entries(e).forEach(([e,o])=>{(!n||n&&!n([...i,e]))&&o!=null&&(typeof o==`object`&&Object.keys(o).length>0?r(o,[...i,e],Array.isArray(o)?[...a,e]:a):t([...i,e],o,a))})}r(e)};var sl=(e,t)=>typeof t==`number`?[`lineHeight`,`fontWeight`,`opacity`,`zIndex`].some(t=>e.includes(t))||e[e.length-1].toLowerCase().includes(`opacity`)?t:`${t}px`:t;function cl(e,t){let{prefix:n,shouldSkipGeneratingVar:r}=t||{},i={},a={},o={};return ol(e,(e,t,s)=>{if((typeof t==`string`||typeof t==`number`)&&(!r||!r(e,t))){let r=`--${n?`${n}-`:``}${e.join(`-`)}`,c=sl(e,t);Object.assign(i,{[r]:c}),al(a,e,`var(${r})`,s),al(o,e,`var(${r}, ${c})`,s)}},e=>e[0]===`vars`),{css:i,vars:a,varsWithDefaults:o}}function ll(e,t={}){let{getSelector:n=_,disableCssColorScheme:r,colorSchemeSelector:i,enableContrastVars:a}=t,{colorSchemes:o={},components:s,defaultColorS
Line truncated
|
||||||
|
|
||||||
|
const theme = ${JSON.stringify(t,null,2)};
|
||||||
|
|
||||||
|
theme.breakpoints = createBreakpoints(theme.breakpoints || {});
|
||||||
|
theme.transitions = createTransitions(theme.transitions || {});
|
||||||
|
|
||||||
|
export default theme;`}function Cu(e){return typeof e==`number`?`${(e*100).toFixed(0)}%`:`calc((${e}) * 100%)`}var wu=e=>{if(!Number.isNaN(+e))return+e;let t=e.match(/\d*\.?\d+/g);if(!t)return 0;let n=0;for(let e=0;e<t.length;e+=1)n+=+t[e];return n};function Tu(e){Object.assign(e,{alpha(t,n){let r=this||e;return r.colorSpace?`oklch(from ${t} l c h / ${typeof n==`string`?`calc(${n})`:n})`:r.vars?`rgba(${t.replace(/var\(--([^,\s)]+)(?:,[^)]+)?\)+/g,`var(--$1Channel)`)} / ${typeof n==`string`?`calc(${n})`:n})`:Vc(t,wu(n))},lighten(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #fff ${Cu(n)})`:Wc(t,n)},darken(t,n){let r=this||e;return r.colorSpace?`color-mix(in ${r.colorSpace}, ${t}, #000 ${Cu(n)})`:Uc(t,n)}})}function Eu(e={},...t){let{breakpoints:n,mixins:r={},spacing:i,palette:a={},transitions:o={},typography:s={},shape:c,colorSpace:l,...u}=e;if(e.vars&&e.generateThemeVars===void 0)throw Error(ai(20));let d=ru({...a,colorSpace:l}),f=Ks(e),p=So(f,{mixins:au(f.breakpoints,r),palette:d,shadows:mu.slice(),typography:lu(d,s),transitions:yu(o),zIndex:{...bu}});return p=So(p,u),p=t.reduce((e,t)=>So(e,t),p),p.unstable_sxConfig={...Rs,...u?.unstable_sxConfig},p.unstable_sx=function(e){return Us({sx:e,theme:this})},p.toRuntimeSource=Su,Tu(p),p}var Du=Eu;function Ou(e){let t;return t=e<1?5.11916*e**2:4.5*Math.log(e+1)+2,Math.round(t*10)/1e3}var ku=[...Array(25)].map((e,t)=>{if(t===0)return`none`;let n=Ou(t);return`linear-gradient(rgba(255 255 255 / ${n}), rgba(255 255 255 / ${n}))`});function Au(e){return{inputPlaceholder:e===`dark`?.5:.42,inputUnderline:e===`dark`?.7:.42,switchTrackDisabled:e===`dark`?.2:.12,switchTrack:e===`dark`?.3:.38}}function ju(e){return e===`dark`?ku:[]}function Mu(e){let{palette:t={mode:`light`},opacity:n,overlays:r,colorSpace:i,...a}=e,o=ru({...t,colorSpace:i});return{palette:o,opacity:{...Au(o.mode),...n},overlays:r||ju(o.mode),...a}}function Nu(e){return!!e[0].match(/(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/)||!!e[0].match(/sxConfig$/)||e[0]===`palette`&&!!e[1]?.match(/(mode|contrastThreshold|tonalOffset)/)}var Pu=e=>[...[...Array(25)].map((t,n)=>`--${e?`${e}-`:``}overlays-${n}`),`--${e?`${e}-`:``}palette-AppBar-darkBg`,`--${e?`${e}-`:``}palette-AppBar-darkColor`],Fu=e=>(t,n)=>{let r=e.rootSelector||`:root`,i=e.colorSchemeSelector,a=i;if(i===`class`&&(a=`.%s`),i===`data`&&(a=`[data-%s]`),i?.startsWith(`data-`)&&!i.includes(`%s`)&&(a=`[${i}="%s"]`),e.defaultColorScheme===t){if(t===`dark`){let i={};return Pu(e.cssVarPrefix).forEach(e=>{i[e]=n[e],delete n[e]}),a===`media`?{[r]:n,"@media (prefers-color-scheme: dark)":{[r]:i}}:a?{[a.replace(`%s`,t)]:i,[`${r}, ${a.replace(`%s`,t)}`]:n}:{[r]:{...n,...i}}}if(a&&a!==`media`)return`${r}, ${a.replace(`%s`,String(t))}`}else if(t){if(a===`media`)return{[`@media (prefers-color-scheme: ${String(t)})`]:{[r]:n}};if(a)return a.replace(`%s`,String(t))}return r};function Iu(e,t){t.forEach(t=>{e[t]||(e[t]={})})}function q(e,t,n){!e[t]&&n&&(e[t]=n)}function Lu(e){return typeof e!=`string`||!e.startsWith(`hsl`)?e:Rc(e)}function Ru(e,t){`${t}Channel`in e||(e[`${t}Channel`]=Ic(Lu(e[t]),`MUI: Can't create \`palette.${t}Channel\` because \`palette.${t}\` is not one of these formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().
|
||||||
|
To suppress this warning, you need to explicitly provide the \`palette.${t}Channel\` as a string (in rgb format, for example "12 12 12") or undefined if you want to remove the channel token.`))}function zu(e){return typeof e==`number`?`${e}px`:typeof e==`string`||typeof e==`function`||Array.isArray(e)?e:`8px`}var Bu=e=>{try{return e()}catch{}};const Vu=(e=`mui`)=>il(e);function Hu(e,t,n,r,i){if(!n)return;n=n===!0?{}:n;let a=i===`dark`?`dark`:`light`;if(!r){t[i]=Mu({...n,palette:{mode:a,...n?.palette},colorSpace:e});return}let{palette:o,...s}=Du({...r,palette:{mode:a,...n?.palette},colorSpace:e});return t[i]={...n,palette:o,opacity:{...Au(a),...n?.opacity},overlays:n?.overlays||ju(a)},s}function Uu(e={},...t){let{colorSchemes:n={light:!0},defaultColorScheme:r,disableCssColorScheme:i=!1,cssVarPrefix:a=`mui`,nativeColor:o=!1,shouldSkipGeneratingVar:s=Nu,colorSchemeSelector:c=n.light&&n.dark?`media`:void 0,rootSelector:l=`:root`,...u}=e,d=Object.keys(n)[0],f=r||(n.light&&d!==`light`?`light`:d),p=Vu(a),{[f]:m,light:h,dark:g,..._}=n,v={..._},y=m;if((f===`dark`&&!(`dark`in n)||f===`light`&&!(`light`in n))&&(y=!0),!y)throw Error(ai(21,f));let b;o&&(b=`oklch`);let x=Hu(b,v,y,u,f);h&&!v.light&&Hu(b,v,h,void 0,`light`),g&&!v.dark&&Hu(b,v,g,void 0,`dark`);let S={defaultColorScheme:f,...x,cssVarPrefix:a,colorSchemeSelector:c,rootSelector:l,getCssVar:p,colorSchemes:v,font:{...iu(x.typography),...x.font},spacing:zu(u.spacing)};Object.keys(S.colorSchemes).forEach(e=>{let t=S.colorSchemes[e].palette,n=e=>{let n=e.split(`-`),r=n[1],i=n[2];return p(e,t[r][i])};t.mode===`light`&&(q(t.common,`background`,`#fff`),q(t.common,`onBackground`,`#000`)),t.mode===`dark`&&(q(t.common,`background`,`#000`),q(t.common,`onBackground`,`#fff`));function r(e,t,n){if(b){let r;return e===Hc&&(r=`transparent ${((1-n)*100).toFixed(0)}%`),e===H&&(r=`#000 ${(n*100).toFixed(0)}%`),e===U&&(r=`#fff ${(n*100).toFixed(0)}%`),`color-mix(in ${b}, ${t}, ${r})`}return e(t,n)}if(Iu(t,[`Alert`,`AppBar`,`Avatar`,`Button`,`Chip`,`FilledInput`,`LinearProgress`,`Skeleton`,`Slider`,`SnackbarContent`,`SpeedDialAction`,`StepConnector`,`StepContent`,`Switch`,`TableCell`,`Tooltip`]),t.mode===`light`){q(t.Alert,`errorColor`,r(H,t.error.light,.6)),q(t.Alert,`infoColor`,r(H,t.info.light,.6)),q(t.Alert,`successColor`,r(H,t.success.light,.6)),q(t.Alert,`warningColor`,r(H,t.warning.light,.6)),q(t.Alert,`errorFilledBg`,n(`palette-error-main`)),q(t.Alert,`infoFilledBg`,n(`palette-info-main`)),q(t.Alert,`successFilledBg`,n(`palette-success-main`)),q(t.Alert,`warningFilledBg`,n(`palette-warning-main`)),q(t.Alert,`errorFilledColor`,Bu(()=>t.getContrastText(t.error.main))),q(t.Alert,`infoFilledColor`,Bu(()=>t.getContrastText(t.info.main))),q(t.Alert,`successFilledColor`,Bu(()=>t.getContrastText(t.success.main))),q(t.Alert,`warningFilledColor`,Bu(()=>t.getContrastText(t.warning.main))),q(t.Alert,`errorStandardBg`,r(U,t.error.light,.9)),q(t.Alert,`infoStandardBg`,r(U,t.info.light,.9)),q(t.Alert,`successStandardBg`,r(U,t.success.light,.9)),q(t.Alert,`warningStandardBg`,r(U,t.warning.light,.9)),q(t.Alert,`errorIconColor`,n(`palette-error-main`)),q(t.Alert,`infoIconColor`,n(`palette-info-main`)),q(t.Alert,`successIconColor`,n(`palette-success-main`)),q(t.Alert,`warningIconColor`,n(`palette-warning-main`)),q(t.AppBar,`defaultBg`,n(`palette-grey-100`)),q(t.Avatar,`defaultBg`,n(`palette-grey-400`)),q(t.Button,`inheritContainedBg`,n(`palette-grey-300`)),q(t.Button,`inheritContainedHoverBg`,n(`palette-grey-A100`)),q(t.Chip,`defaultBorder`,n(`palette-grey-400`)),q(t.Chip,`defaultAvatarColor`,n(`palette-grey-700`)),q(t.Chip,`defaultIconColor`,n(`palette-grey-700`)),q(t.FilledInput,`bg`,`rgba(0, 0, 0, 0.06)`),q(t.FilledInput,`hoverBg`,`rgba(0, 0, 0, 0.09)`),q(t.FilledInput,`disabledBg`,`rgba(0, 0, 0, 0.12)`),q(t.LinearProgress,`primaryBg`,r(U,t.primary.main,.62)),q(t.LinearProgress,`secondaryBg`,r(U,t.secondary.main,.62)),q(t.LinearProgress,`errorBg`,r(U,t.error.main,.62)),q(t.LinearProgress,`infoBg`,r(U,t.info.main,.62)),q(t.LinearProgress,`successBg`,r(U,t.success.main,.62)),q(t.LinearProgress,`warningBg`,r(U,t.warning.main,.62)),q(t.Skeleton,`bg`,b?r(Hc,t.text.primary,.11):`rgba(${n(`palette-text-primaryChannel`)} / 0.11)`),q(t.Slider,`primaryTrack`,r(U,t.primary.main,.62)),q(t.Slider,`secondaryTrack`,r(U,t.secondary.main,.62)),q(t.Slider,`errorTrack`,r(U,t.error.main,.62)),q(t.Slider,`infoTrack`,r(U,t.info.main,.62)),q(t.Slider,`successTrack`,r(U,t.success.main,.62)),q(t.Slider,`warningTrack`,r(U,t.warning.main,.62));let e=b?r(H,t.background.default,.6825):Kc(t.background.default,.8);q(t.SnackbarContent,`bg`,e),q(t.SnackbarContent,`color`,Bu(()=>b?ql.text.primary:t.getContrastText(e))),q(t.SpeedDialAction,`fabHoverBg`,Kc(t.background.paper,.15)),q(t.StepConnector,`border`,n(`palette-grey-400`)),q(t.StepContent,`border`,n(`palette-grey-400`)),q(t.Switch,`defaultColor`,n(`palette-common-white`)),q(t.Switch,`defaultDisabledColor`,n(`palette-grey-100`)),q(t.Switch,`primaryDisabledColor`,r(U,tLine truncated
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
`,kf=eo`
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,Af=eo`
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
`;const jf=J(`span`,{name:`MuiTouchRipple`,slot:`Root`})({overflow:`hidden`,pointerEvents:`none`,position:`absolute`,zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:`inherit`}),Mf=J(Tf,{name:`MuiTouchRipple`,slot:`Ripple`})`
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&.${Ef.rippleVisible} {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
animation-name: ${Of};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.${Ef.ripplePulsate} {
|
||||||
|
animation-duration: ${({theme:e})=>e.transitions.duration.shorter}ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.child} {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childLeaving} {
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: ${kf};
|
||||||
|
animation-duration: ${Df}ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
}
|
||||||
|
|
||||||
|
& .${Ef.childPulsate} {
|
||||||
|
position: absolute;
|
||||||
|
/* @noflip */
|
||||||
|
left: 0px;
|
||||||
|
top: 0;
|
||||||
|
animation-name: ${Af};
|
||||||
|
animation-duration: 2500ms;
|
||||||
|
animation-timing-function: ${({theme:e})=>e.transitions.easing.easeInOut};
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
`;var Nf=x.forwardRef(function(e,t){let{center:n=!1,classes:r={},className:i,...a}=X({props:e,name:`MuiTouchRipple`}),[o,s]=x.useState([]),c=x.useRef(0),l=x.useRef(null);x.useEffect(()=>{l.current&&=(l.current(),null)},[o]);let u=x.useRef(!1),d=$d(),f=x.useRef(null),p=x.useRef(null),m=x.useCallback(e=>{let{pulsate:t,rippleX:n,rippleY:i,rippleSize:a,cb:o}=e;s(e=>[...e,(0,V.jsx)(Mf,{classes:{ripple:B(r.ripple,Ef.ripple),rippleVisible:B(r.rippleVisible,Ef.rippleVisible),ripplePulsate:B(r.ripplePulsate,Ef.ripplePulsate),child:B(r.child,Ef.child),childLeaving:B(r.childLeaving,Ef.childLeaving),childPulsate:B(r.childPulsate,Ef.childPulsate)},timeout:Df,pulsate:t,rippleX:n,rippleY:i,rippleSize:a},c.current)]),c.current+=1,l.current=o},[r]),h=x.useCallback((e={},t={},r=()=>{})=>{let{pulsate:i=!1,center:a=n||t.pulsate,fakeElement:o=!1}=t;if(e?.type===`mousedown`&&u.current){u.current=!1;return}e?.type===`touchstart`&&(u.current=!0);let s=o?null:p.current,c=s?s.getBoundingClientRect():{width:0,height:0,left:0,top:0},l,h,g;if(a||e===void 0||e.clientX===0&&e.clientY===0||!e.clientX&&!e.touches)l=Math.round(c.width/2),h=Math.round(c.height/2);else{let{clientX:t,clientY:n}=e.touches&&e.touches.length>0?e.touches[0]:e;l=Math.round(t-c.left),h=Math.round(n-c.top)}if(a)g=Math.sqrt((2*c.width**2+c.height**2)/3),g%2==0&&(g+=1);else{let e=Math.max(Math.abs((s?s.clientWidth:0)-l),l)*2+2,t=Math.max(Math.abs((s?s.clientHeight:0)-h),h)*2+2;g=Math.sqrt(e**2+t**2)}e?.touches?f.current===null&&(f.current=()=>{m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},d.start(80,()=>{f.current&&=(f.current(),null)})):m({pulsate:i,rippleX:l,rippleY:h,rippleSize:g,cb:r})},[n,m,d]),g=x.useCallback(()=>{h({},{pulsate:!0})},[h]),_=x.useCallback((e,t)=>{if(d.clear(),e?.type===`touchend`&&f.current){f.current(),f.current=null,d.start(0,()=>{_(e,t)});return}f.current=null,s(e=>e.length>0?e.slice(1):e),l.current=t},[d]);return x.useImperativeHandle(t,()=>({pulsate:g,start:h,stop:_}),[g,h,_]),(0,V.jsx)(jf,{className:B(Ef.root,r.root,i),ref:p,...a,children:(0,V.jsx)(qd,{component:null,exit:!0,children:o})})});function Pf(e){return sc(`MuiButtonBase`,e)}var Ff=cc(`MuiButtonBase`,[`root`,`disabled`,`focusVisible`]),If=e=>{let{disabled:t,focusVisible:n,focusVisibleClassName:r,classes:i}=e,a=fl({root:[`root`,t&&`disabled`,n&&`focusVisible`]},Pf,i);return n&&r&&(a.root+=` ${r}`),a};const Lf=J(`button`,{name:`MuiButtonBase`,slot:`Root`})({display:`inline-flex`,alignItems:`center`,justifyContent:`center`,position:`relative`,boxSizing:`border-box`,WebkitTapHighlightColor:`transparent`,backgroundColor:`transparent`,outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:`pointer`,userSelect:`none`,verticalAlign:`middle`,MozAppearance:`none`,WebkitAppearance:`none`,textDecoration:`none`,color:`inherit`,"&::-moz-focus-inner":{borderStyle:`none`},[`&.${Ff.disabled}`]:{pointerEvents:`none`,cursor:`default`},"@media print":{colorAdjust:`exact`}});var Rf=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiButtonBase`}),{action:r,centerRipple:i=!1,children:a,className:o,component:s=`button`,disabled:c=!1,disableRipple:l=!1,disableTouchRipple:u=!1,focusRipple:d=!1,focusVisibleClassName:f,LinkComponent:p=`a`,onBlur:m,onClick:h,onContextMenu:g,onDragLeave:_,onFocus:v,onFocusVisible:y,onKeyDown:b,onKeyUp:S,onMouseDown:C,onMouseLeave:w,onMouseUp:T,onTouchEnd:E,onTouchMove:D,onTouchStart:O,tabIndex:ee=0,TouchRippleProps:k,touchRippleRef:A,type:te,...ne}=n,re=x.useRef(null),j=Sf(),M=Z(j.ref,A),[N,ie]=x.useState(!1);c&&N&&ie(!1),x.useImperativeHandle(r,()=>({focusVisible:()=>{ie(!0),re.current.focus()}}),[]);let ae=j.shouldMount&&!l&&!c;x.useEffect(()=>{N&&d&&!l&&j.pulsate()},[l,d,N,j]);let oe=zf(j,`start`,C,u),se=zf(j,`stop`,g,u),ce=zf(j,`stop`,_,u),P=zf(j,`stop`,T,u),le=zf(j,`stop`,e=>{N&&e.preventDefault(),w&&w(e)},u),ue=zf(j,`start`,O,u),de=zf(j,`stop`,E,u),fe=zf(j,`stop`,D,u),pe=zf(j,`stop`,e=>{bf(e.target)||ie(!1),m&&m(e)},!1),me=yd(e=>{re.current||=e.currentTarget,bf(e.target)&&(ie(!0),y&&y(e)),v&&v(e)}),he=()=>{let e=re.current;return s&&s!==`button`&&!(e.tagName===`A`&&e.href)},ge=yd(e=>{d&&!e.repeat&&N&&e.key===` `&&j.stop(e,()=>{j.start(e)}),e.target===e.currentTarget&&he()&&e.key===` `&&e.preventDefault(),b&&b(e),e.target===e.currentTarget&&he()&&e.key===`Enter`&&!c&&(e.preventDefault(),h&&h(e))}),_e=yd(e=>{d&&e.key===` `&&N&&!e.defaultPrevented&&j.stop(e,()=>{j.pulsate(e)}),S&&S(e),h&&e.target===e.currentTarget&&he()&&e.key===` `&&!e.defaultPrevented&&h(e)}),ve=s;ve===`button`&&(ne.href||ne.to)&&(ve=p);let ye={};ve===`button`?(ye.type=te===void 0?`button`:te,ye.disabled=c):(!ne.href&&!ne.to&&(ye.role=`button`),c&&(ye[`aria-disabled`]=c));let be=Z(t,re),xe={...n,centerRipple:i,component:s,disabled:c,disableRipple:l,disableTouchRipple:u,focusRipple:d,tabIndex:ee,focusVisible:N},Se=If(xe);return(0,V.jsxs)(Lf,{as:ve,className:B(Se.root,o),ownerState:xe,onBlur:pe,onClick:h,onContextMenu:se,onFocus:me,onKeyDown:ge,onKeyUp:_e,onMouseDown:oe,onMouseLeave:le,onMous
Line truncated
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`,Yf=eo`
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 100px, 200px;
|
||||||
|
stroke-dashoffset: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 1px, 200px;
|
||||||
|
stroke-dashoffset: -126px;
|
||||||
|
}
|
||||||
|
`,Xf=typeof Jf==`string`?null:$a`
|
||||||
|
animation: ${Jf} 1.4s linear infinite;
|
||||||
|
`,Zf=typeof Yf==`string`?null:$a`
|
||||||
|
animation: ${Yf} 1.4s ease-in-out infinite;
|
||||||
|
`,Qf=e=>{let{classes:t,variant:n,color:r,disableShrink:i}=e;return fl({root:[`root`,n,`color${Y(r)}`],svg:[`svg`],track:[`track`],circle:[`circle`,`circle${Y(n)}`,i&&`circleDisableShrink`]},Kf,t)},$f=J(`span`,{name:`MuiCircularProgress`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,t[n.variant],t[`color${Y(n.color)}`]]}})($u(({theme:e})=>({display:`inline-block`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`transform`)}},{props:{variant:`indeterminate`},style:Xf||{animation:`${Jf} 1.4s linear infinite`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}}))]}))),ep=J(`svg`,{name:`MuiCircularProgress`,slot:`Svg`})({display:`block`}),tp=J(`circle`,{name:`MuiCircularProgress`,slot:`Circle`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.circle,t[`circle${Y(n.variant)}`],n.disableShrink&&t.circleDisableShrink]}})($u(({theme:e})=>({stroke:`currentColor`,variants:[{props:{variant:`determinate`},style:{transition:e.transitions.create(`stroke-dashoffset`)}},{props:{variant:`indeterminate`},style:{strokeDasharray:`80px, 200px`,strokeDashoffset:0}},{props:({ownerState:e})=>e.variant===`indeterminate`&&!e.disableShrink,style:Zf||{animation:`${Yf} 1.4s ease-in-out infinite`}}]}))),np=J(`circle`,{name:`MuiCircularProgress`,slot:`Track`})($u(({theme:e})=>({stroke:`currentColor`,opacity:(e.vars||e).palette.action.activatedOpacity}))),rp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiCircularProgress`}),{className:r,color:i=`primary`,disableShrink:a=!1,enableTrackSlot:o=!1,size:s=40,style:c,thickness:l=3.6,value:u=0,variant:d=`indeterminate`,...f}=n,p={...n,color:i,disableShrink:a,size:s,thickness:l,value:u,variant:d,enableTrackSlot:o},m=Qf(p),h={},g={},_={};if(d===`determinate`){let e=2*Math.PI*((qf-l)/2);h.strokeDasharray=e.toFixed(3),_[`aria-valuenow`]=Math.round(u),h.strokeDashoffset=`${((100-u)/100*e).toFixed(3)}px`,g.transform=`rotate(-90deg)`}return(0,V.jsx)($f,{className:B(m.root,r),style:{width:s,height:s,...g,...c},ownerState:p,ref:t,role:`progressbar`,..._,...f,children:(0,V.jsxs)(ep,{className:m.svg,ownerState:p,viewBox:`${qf/2} ${qf/2} ${qf} ${qf}`,children:[o?(0,V.jsx)(np,{className:m.track,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l,"aria-hidden":`true`}):null,(0,V.jsx)(tp,{className:m.circle,style:h,ownerState:p,cx:qf,cy:qf,r:(qf-l)/2,fill:`none`,strokeWidth:l})]})})});function ip(e){return sc(`MuiIconButton`,e)}var ap=cc(`MuiIconButton`,[`root`,`disabled`,`colorInherit`,`colorPrimary`,`colorSecondary`,`colorError`,`colorInfo`,`colorSuccess`,`colorWarning`,`edgeStart`,`edgeEnd`,`sizeSmall`,`sizeMedium`,`sizeLarge`,`loading`,`loadingIndicator`,`loadingWrapper`]),op=e=>{let{classes:t,disabled:n,color:r,edge:i,size:a,loading:o}=e;return fl({root:[`root`,o&&`loading`,n&&`disabled`,r!==`default`&&`color${Y(r)}`,i&&`edge${Y(i)}`,`size${Y(a)}`],loadingIndicator:[`loadingIndicator`],loadingWrapper:[`loadingWrapper`]},ip,t)},sp=J(Bf,{name:`MuiIconButton`,slot:`Root`,overridesResolver:(e,t)=>{let{ownerState:n}=e;return[t.root,n.loading&&t.loading,n.color!==`default`&&t[`color${Y(n.color)}`],n.edge&&t[`edge${Y(n.edge)}`],t[`size${Y(n.size)}`]]}})($u(({theme:e})=>({textAlign:`center`,flex:`0 0 auto`,fontSize:e.typography.pxToRem(24),padding:8,borderRadius:`50%`,color:(e.vars||e).palette.action.active,transition:e.transitions.create(`background-color`,{duration:e.transitions.duration.shortest}),variants:[{props:e=>!e.disableRipple,style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette.action.active,(e.vars||e).palette.action.hoverOpacity),"&:hover":{backgroundColor:`var(--IconButton-hoverBg)`,"@media (hover: none)":{backgroundColor:`transparent`}}}},{props:{edge:`start`},style:{marginLeft:-12}},{props:{edge:`start`,size:`small`},style:{marginLeft:-3}},{props:{edge:`end`},style:{marginRight:-12}},{props:{edge:`end`,size:`small`},style:{marginRight:-3}}]})),$u(({theme:e})=>({variants:[{props:{color:`inherit`},style:{color:`inherit`}},...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{color:(e.vars||e).palette[t].main}})),...Object.entries(e.palette).filter(Uf()).map(([t])=>({props:{color:t},style:{"--IconButton-hoverBg":e.alpha((e.vars||e).palette[t].main,(e.vars||e).palette.action.hoverOpacity)}})),{props:{size:`small`},style:{padding:5,fontSize:e.typography.pxToRem(18)}},{props:{size:`large`},style:{padding:12,fontSize:e.typography.pxToRem(28)}}],[`&.${ap.disabled}`]:{backgroundColor:`transparent`,color:(e.vars||e).palette.action.disabled},[`&.${ap.loading}`]:{color:`transparent`}}))),cp=J(`span`,{name:`MuiIconButton`,slot:`LoadingIndicator`})(({theme:e})=>({display:`none`,position:`absolute`,visibility:`visible`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%)`,color:(e.vars||e).palette.action.disabled,variants:[{props:{loading:!0},style:{display:`flex`}}]})),lp=x.forwardRef(function(e,t){let n=X({props:e,name:`MuiIconButton`}
Line truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;color:#ffffffde;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#242424;font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;font-weight:400;line-height:1.5}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}a{color:#646cff;-webkit-text-decoration:inherit;text-decoration:inherit;font-weight:500}a:hover{color:#535bf2}body{place-items:center;min-width:320px;min-height:100vh;margin:0;display:flex}h1{font-size:3.2em;line-height:1.1}button{cursor:pointer;background-color:#1a1a1a;border:1px solid #0000;border-radius:8px;padding:.6em 1.2em;font-family:inherit;font-size:1em;font-weight:500;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.vitec-element{box-shadow:none;color:inherit;background-color:#0000;background-image:none;border:none;border-radius:0;margin-top:.5rem;margin-bottom:.5rem;padding:0;position:relative}@media (min-width:900px){.vitec-element--full-width{width:calc(100% + 4rem);margin-left:-2rem}}.vitec-element__stack{flex-direction:column;display:flex}.vitec-space-before--extra-small{margin-top:.5rem}.vitec-space-before--small{margin-top:1rem}.vitec-space-before--medium{margin-top:2.5rem}.vitec-space-before--large{margin-top:4rem}.vitec-space-before--extra-large{margin-top:6rem}.vitec-space-after--extra-small{margin-bottom:.5rem}.vitec-space-after--small{margin-bottom:1rem}.vitec-space-after--medium{margin-bottom:2.5rem}.vitec-space-after--large{margin-bottom:4rem}.vitec-space-after--extra-large{margin-bottom:6rem}*,:before,:after{box-sizing:border-box}body,html{color:#1f2937;background:#f4f7fb;width:100%;min-height:100vh;margin:0;font-family:Roboto,system-ui,-apple-system,sans-serif}#page{flex-direction:column;width:100%;min-height:100vh;display:flex}#header{color:#f8fafc;background:linear-gradient(120deg,#26358cbf 20%,#f4793680 100%);flex-direction:column;justify-content:center;gap:1rem;width:100%;min-height:300px;padding:1rem 1.75rem;display:flex;box-shadow:0 12px 30px #0f274947}.header-chrome{z-index:2;flex-direction:column;gap:1rem;display:flex;position:relative}#header.header--with-hero{min-height:0;box-shadow:none;background:0 0;justify-content:flex-start;gap:0;padding:0;position:relative;overflow:hidden}#header.header--with-hero .header-chrome{padding:1.65rem 1.75rem 0;position:absolute;inset:0 0 auto}.header-hero{color:#f8fafc;background:#0f172a;width:100%;min-height:620px;margin-left:0;position:relative;top:0;overflow:hidden}.header-hero-media,.header-hero-overlay{position:absolute;inset:0}.header-hero-media{opacity:.45;background-position:50%;background-repeat:no-repeat;background-size:cover}.header-hero-overlay{background:linear-gradient(120deg,#26358cc7 20%,#f4793694 100%)}.header-hero-content{z-index:1;align-items:center;min-height:620px;padding-top:9.5rem;padding-bottom:4rem;display:flex;position:relative}.header-hero-copy{line-height:1.6}.header-hero-copy p{margin:0 0 1rem}.header-hero-copy p:last-of-type{margin-bottom:0}.header-hero-copy a{color:inherit}.header-hero-copy mark{color:inherit;background:#facc15d9;padding:0 .2rem}.header-hero-image-sr{clip:rect(0,0,0,0);border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.header-top-row{justify-content:space-between;align-items:center;gap:1rem;width:100%;display:flex}.header-logo-link{align-items:center;display:inline-flex}.header-logo{object-fit:contain;width:auto;height:34px}.mobile-nav-toggle{color:#f8fafc;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:#0f172a33;border:1px solid #f8fafc2e;border-radius:999px;min-width:auto;padding:.6rem;display:none}.mobile-nav-toggle:hover{background:#0f172a52}.mobile-nav-toggle__icon{flex-direction:column;justify-content:space-between;width:1.3rem;height:1rem;display:inline-flex}.mobile-nav-toggle__icon span{transform-origin:50%;background:currentColor;border-radius:999px;width:100%;height:2px;transition:transform .2s,opacity .2s;display:block}.mobile-nav-toggle__icon--open span:first-child{transform:translateY(7px)rotate(45deg)}.mobile-nav-toggle__icon--open span:nth-child(2){opacity:0}.mobile-nav-toggle__icon--open span:nth-child(3){transform:translateY(-7px)rotate(-45deg)}.mobile-nav-drawer{color:#f8fafc;background:linear-gradient(#0f172a 0%,#1e3a8a 100%);border-left:1px solid #f8fafc1f;width:min(24rem,100vw)}.mobile-nav-drawer__inner{min-height:100%;padding:1.25rem}.mobile-nav-drawer__header{justify-content:space-between;align-items:center;gap:1rem;margin-bottom:1.25rem;display:flex}.mobile-nav-close{color:#f8fafc;border:1px solid #f8fafc24;border-radius:999px;min-width:auto;padding:.35rem .85rem}.mobilLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;color:#ffffffde;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#242424;font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;font-weight:400;line-height:1.5}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}a{color:#646cff;-webkit-text-decoration:inherit;text-decoration:inherit;font-weight:500}a:hover{color:#535bf2}body{place-items:center;min-width:320px;min-height:100vh;margin:0;display:flex}h1{font-size:3.2em;line-height:1.1}button{cursor:pointer;background-color:#1a1a1a;border:1px solid #0000;border-radius:8px;padding:.6em 1.2em;font-family:inherit;font-size:1em;font-weight:500;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.vitec-element{box-shadow:none;color:inherit;background-color:#0000;background-image:none;border:none;border-radius:0;margin-top:.5rem;margin-bottom:.5rem;padding:0;position:relative}@media (min-width:900px){.vitec-element--full-width{width:calc(100% + 4rem);margin-left:-2rem}}.vitec-element__stack{flex-direction:column;display:flex}.vitec-space-before--extra-small{margin-top:.5rem}.vitec-space-before--small{margin-top:1rem}.vitec-space-before--medium{margin-top:2.5rem}.vitec-space-before--large{margin-top:4rem}.vitec-space-before--extra-large{margin-top:6rem}.vitec-space-after--extra-small{margin-bottom:.5rem}.vitec-space-after--small{margin-bottom:1rem}.vitec-space-after--medium{margin-bottom:2.5rem}.vitec-space-after--large{margin-bottom:4rem}.vitec-space-after--extra-large{margin-bottom:6rem}.vitec_card__link-wrap{color:inherit;text-decoration:none;display:block}.vitec_card__link-wrap:hover .vitec_card__inner{transform:translateY(-2px);box-shadow:0 6px 18px #0f274926}.vitec_card__inner{background:#fff;border-radius:.75rem;flex-direction:column;height:100%;transition:box-shadow .2s,transform .2s;display:flex;overflow:hidden;box-shadow:0 2px 10px #0f274917}.vitec_card__inner:hover{transform:translateY(-2px);box-shadow:0 6px 18px #0f274926}.vitec_card__inner.vitec_card__inner--centered{text-align:center;align-items:center}.vitec_card__media{aspect-ratio:16/7;flex-shrink:0;width:100%;overflow:hidden}.vitec_card__image{object-fit:cover;width:100%;height:100%;display:block}.vitec_card__body{flex-direction:column;flex:1;gap:.5rem;padding:1rem;display:flex}.vitec_card__eyebrow{opacity:.65;letter-spacing:.1em;font-size:.75rem}.vitec_card__header{color:#f47936;font-size:1.1rem;font-weight:600;line-height:1.3}.vitec_card__subheader{color:#6b7280;font-size:.8125rem}.vitec_card__text{color:#374151;flex:1;font-size:.875rem;line-height:1.55}.vitec_card__text p:first-of-type{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.vitec_card__text p+p{display:none}.vitec_card__footer{margin-top:auto;padding-top:.75rem}.vitec_card__footer .MuiButton-contained{color:#fff;letter-spacing:.08em;background-color:#f47936;border-radius:.4rem;width:100%;padding:.6rem 1rem;font-weight:700}.vitec_card__footer .MuiButton-contained:hover{background-color:#d9641f}.vitec_card__footer .MuiButton-outlined{color:#f47936;letter-spacing:.05em;border-color:#f47936;border-radius:.4rem;width:100%;padding:.55rem 1rem;font-weight:600}.vitec_card__footer .MuiButton-outlined:hover{background-color:#f479360f}*,:before,:after{box-sizing:border-box}body,html{color:#1f2937;background:#f4f7fb;width:100%;min-height:100vh;margin:0;font-family:Roboto,system-ui,-apple-system,sans-serif}#page{flex-direction:column;width:100%;min-height:100vh;display:flex}#header{color:#f8fafc;background:linear-gradient(120deg,#26358cbf 20%,#f4793680 100%);flex-direction:column;justify-content:center;gap:1rem;width:100%;min-height:300px;padding:1rem 1.75rem;display:flex;box-shadow:0 12px 30px #0f274947}.header-chrome{z-index:2;flex-direction:column;gap:1rem;display:flex;position:relative}#header.header--with-hero{min-height:0;box-shadow:none;background:0 0;justify-content:flex-start;gap:0;padding:0;position:relative;overflow:hidden}#header.header--with-hero .header-chrome{padding:1.65rem 1.75rem 0;position:absolute;inset:0 0 auto}.header-hero{color:#f8fafc;background:#0f172a;width:100%;min-height:620px;margin-left:0;position:relative;top:0;overflow:hidden}.header-hero-media,.header-hero-overlay{position:absolute;inset:0}.header-hero-media{opacity:.45;background-position:50%;background-repeat:no-repeat;background-size:cover}.header-hero-overlay{background:linear-gradient(120deg,#26358cc7 20%,#f4793694 100%)}.header-hero-content{z-index:1;align-items:center;min-height:620px;padding-top:9.5rem;padding-bottom:4rem;display:flex;position:relative}.header-hero-copy{line-height:1.6}.header-herLine truncated
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<title>VITEC</title>
|
<title>VITEC</title>
|
||||||
<script type="module" crossorigin src="/_frontend/assets/index-QAXHHeve.js"></script>
|
<script type="module" crossorigin src="/_frontend/assets/index-5XMvTCBl.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-B-ARIO24.css">
|
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-JMQ9Ot1Q.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page"></div>␍
|
<div id="page"></div>␍
|
||||||
|
|||||||