From eae40f42867935c0e25c96cbf83a62b415847d1f Mon Sep 17 00:00:00 2001 From: khaccount Date: Fri, 24 Jul 2026 09:53:33 +0200 Subject: [PATCH] Success-story migration + new plugins + JSON/UX polish - Success Stories: full migration from old site (48/48, audited), markets n:n, quotation content block, columns content block, pretty SEO detail URLs via SuccessStoryPathRewrite middleware, list/detail split (list page 4 / story page), detailUrl/backUrl - New plugins: VITEC Locations (grid/list/map + RTE map text), VITEC Customer Logos (color/bw logic, only-show-selected), VITEC Card (one plugin for product/story/market/solution with reloading FlexForm + custom backend preview renderer) - Eventlist: layout dropdown (list/grid/teaserbar) in settings - Hero section CB: Images/Video tabs, background video + overlay - Product JSON: full category rootline (parents), fixed missing ConnectionPool import (all-products crash), category tree map - Backend preview CSS: container-query responsive (narrow columns) - Docs: ISO architecture spec, root + extension READMEs Co-Authored-By: Claude Fable 5 --- README.md | 4 + composer.json | 1 + composer.lock | 68 +- config/sites/vitec/config.yaml | 22 + .../Command/ImportSuccessStoriesCommand.php | 708 +++ .../Classes/Controller/CustomerController.php | 21 + .../Classes/Controller/LocationController.php | 21 + .../Controller/ModelcardController.php | 21 + .../ContainerChildrenProcessor.php | 6 + .../Middleware/SuccessStoryPathRewrite.php | 79 + .../Preview/ModelcardPreviewRenderer.php | 127 + .../Service/ContentElementResolver.php | 6 + .../Classes/Service/UsecaseSerializer.php | 166 +- .../UserFunc/CustomerlogosJsonRenderer.php | 232 + .../UserFunc/EventlistJsonRenderer.php | 2 + .../UserFunc/LocationsJsonRenderer.php | 197 + .../UserFunc/ModelcardJsonRenderer.php | 175 + .../UserFunc/ProductListJsonRenderer.php | 49 +- .../UserFunc/ProductShowJsonRenderer.php | 46 + .../UserFunc/UsecaseListJsonRenderer.php | 49 +- .../UserFunc/UsecaseShowJsonRenderer.php | 36 +- .../Configuration/FlexForms/Customerlogos.xml | 73 + .../Configuration/FlexForms/Eventlist.xml | 23 + .../Configuration/FlexForms/Locationlist.xml | 57 + .../Configuration/FlexForms/Modelcard.xml | 146 + .../vitec/Configuration/FlexForms/Usecase.xml | 25 +- .../Configuration/FlexForms/Usecaselist.xml | 11 + packages/vitec/Configuration/Icons.php | 12 + .../Configuration/RequestMiddlewares.php | 20 + .../Sets/Vitecset/setup.typoscript | 36 + .../TCA/Overrides/tt_content.php | 35 + .../TCA/tx_vitec_domain_model_customer.php | 63 + .../TCA/tx_vitec_domain_model_location.php | 168 + .../TCA/tx_vitec_domain_model_usecase.php | 17 +- .../ContentElements/hero-section/config.yaml | 85 +- .../hero-section/language/labels.xlf | 22 + .../ContentElements/quotation/assets/icon.svg | 4 + .../ContentElements/quotation/config.yaml | 44 + .../quotation/language/labels.xlf | 28 + .../quotation/templates/backend-preview.html | 27 + .../quotation/templates/frontend.html | 3 + .../Private/Templates/Preview/Modelcard.html | 36 + .../Resources/Public/Css/backend-preview.css | 42 +- .../Icons/vitec-plugin-customerlogos.svg | 6 + .../Icons/vitec-plugin-locationlist.svg | 4 + .../Public/Icons/vitec-plugin-modelcard.svg | 6 + packages/vitec/ext_localconf.php | 36 + packages/vitec/ext_tables.sql | 40 +- public/.htaccess | 11 +- public/_frontend/.htaccess | 54 +- .../assets/VitecDebugToggle-Bp07RQC2.js | 1 + .../assets/VitecDebugToggle-C76AUijU.js | 1 - public/_frontend/assets/header-BwD3yTr8.js | 1 - public/_frontend/assets/header-CddkKp_g.js | 1 + public/_frontend/assets/index-7LYai_r-.js | 148 - ...{index-CQZZ8YAR.css => index-B6-rfpgz.css} | 2 +- public/_frontend/assets/index-CM0zIqrs.js | 148 + ....min-B_jfdBDa.js => jszip.min-DxLKr7hR.js} | 2 +- .../assets/news_newsdetail-Ch4soZ5k.js | 1 + .../assets/news_newsliststicky-LPL7flHj.js | 1 + public/_frontend/assets/text-C_qxRHF2.js | 1 - public/_frontend/assets/text-CkDHrg9f.js | 1 + public/_frontend/assets/textpic-B_cm494c.js | 1 - public/_frontend/assets/textpic-CaKZ2n8y.js | 1 + .../assets/useControlled-CztbT67m.js | 1 + .../assets/useControlled-OB0I4oAK.js | 1 - .../_frontend/assets/vitec_card-BzgUtAT9.js | 1 + .../_frontend/assets/vitec_card-O7eqyfXI.js | 1 - .../assets/vitec_cards_carousel-DEMyg8kG.js | 1 - .../assets/vitec_cards_carousel-Dt6LcIk8.js | 1 + .../_frontend/assets/vitec_cols-BLma67dc.js | 1 - .../_frontend/assets/vitec_cols-DMOC1vMA.js | 1 + .../assets/vitec_columns-CdE2Ajho.js | 1 + .../assets/vitec_container-CeBDSayn.js | 1 + .../assets/vitec_container-CekQ-Tqd.js | 1 - .../assets/vitec_ctabanner-BSc8V7mB.js | 1 - .../assets/vitec_ctabanner-VXQKSyqL.js | 1 + .../assets/vitec_customerlogos-Dj1_aotY.js | 1 + .../assets/vitec_datasheets-Bu643D_W.js | 1 - .../assets/vitec_datasheets-Cp-zTFxi.js | 1 + .../assets/vitec_divider--SH2uqZD.js | 1 - .../assets/vitec_divider-o_0oeIC9.js | 1 + .../assets/vitec_downloadcard-D2j-JEJt.js | 1 + .../assets/vitec_downloadcard-DZEXBeIZ.js | 1 - .../vitec_downloadcardcollection-7D90DISQ.js | 1 - .../vitec_downloadcardcollection-RsRD4eC3.js | 1 + .../assets/vitec_eventlist-Do7dy7ug.js | 1 - .../assets/vitec_eventlist-PgRIlaon.js | 1 + public/_frontend/assets/vitec_faq-Bii0IQAm.js | 1 - public/_frontend/assets/vitec_faq-C66INAn2.js | 1 + .../assets/vitec_herosection-CEdPd8Wl.js | 1 - .../assets/vitec_herosection-Dl4wV6qo.js | 1 + .../assets/vitec_introparagraph-BDH05wxR.js | 1 - .../assets/vitec_introparagraph-DxC6s5bV.js | 1 + .../assets/vitec_locationlist-BoId6hQ3.js | 2 + .../assets/vitec_locationmap-CO_jtYJV.js | 5253 +++++++++++++++++ .../assets/vitec_marketshow-DQPQbqWc.js | 1 + .../assets/vitec_marketshow-euGpFALW.js | 1 - .../assets/vitec_productlist-ANtC_tOO.js | 1 + .../assets/vitec_productlist-BBvL1vVG.js | 1 - .../assets/vitec_productshow-BpVhrdsH.js | 1 + .../assets/vitec_productshow-CFG_m23a.js | 1 - .../assets/vitec_quotation-BClhID_Y.js | 1 + .../assets/vitec_usecaselist-CD1Zq_wB.js | 1 + .../assets/vitec_usecaseshow-B8AiH3X6.js | 1 - .../assets/vitec_usecaseshow-Il_tsmsc.js | 1 + .../_frontend/assets/vitec_video-1ZivpgZw.js | 1 - .../_frontend/assets/vitec_video-eCUGqb4_.js | 1 + public/_frontend/index.html | 4 +- public/migrations/export_success_stories.php | 0 110 files changed, 8502 insertions(+), 279 deletions(-) create mode 100755 packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php create mode 100755 packages/vitec/Classes/Controller/CustomerController.php create mode 100755 packages/vitec/Classes/Controller/LocationController.php create mode 100755 packages/vitec/Classes/Controller/ModelcardController.php create mode 100755 packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php create mode 100755 packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php create mode 100755 packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php create mode 100755 packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php create mode 100755 packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php create mode 100755 packages/vitec/Configuration/FlexForms/Customerlogos.xml create mode 100755 packages/vitec/Configuration/FlexForms/Locationlist.xml create mode 100755 packages/vitec/Configuration/FlexForms/Modelcard.xml create mode 100755 packages/vitec/Configuration/RequestMiddlewares.php create mode 100755 packages/vitec/Configuration/TCA/tx_vitec_domain_model_customer.php create mode 100755 packages/vitec/Configuration/TCA/tx_vitec_domain_model_location.php create mode 100755 packages/vitec/ContentBlocks/ContentElements/quotation/assets/icon.svg create mode 100755 packages/vitec/ContentBlocks/ContentElements/quotation/config.yaml create mode 100755 packages/vitec/ContentBlocks/ContentElements/quotation/language/labels.xlf create mode 100755 packages/vitec/ContentBlocks/ContentElements/quotation/templates/backend-preview.html create mode 100755 packages/vitec/ContentBlocks/ContentElements/quotation/templates/frontend.html create mode 100755 packages/vitec/Resources/Private/Templates/Preview/Modelcard.html create mode 100755 packages/vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg create mode 100755 packages/vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg create mode 100755 packages/vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg create mode 100644 public/_frontend/assets/VitecDebugToggle-Bp07RQC2.js delete mode 100644 public/_frontend/assets/VitecDebugToggle-C76AUijU.js delete mode 100644 public/_frontend/assets/header-BwD3yTr8.js create mode 100644 public/_frontend/assets/header-CddkKp_g.js delete mode 100644 public/_frontend/assets/index-7LYai_r-.js rename public/_frontend/assets/{index-CQZZ8YAR.css => index-B6-rfpgz.css} (55%) create mode 100644 public/_frontend/assets/index-CM0zIqrs.js rename public/_frontend/assets/{jszip.min-B_jfdBDa.js => jszip.min-DxLKr7hR.js} (93%) create mode 100644 public/_frontend/assets/news_newsdetail-Ch4soZ5k.js create mode 100644 public/_frontend/assets/news_newsliststicky-LPL7flHj.js delete mode 100644 public/_frontend/assets/text-C_qxRHF2.js create mode 100644 public/_frontend/assets/text-CkDHrg9f.js delete mode 100644 public/_frontend/assets/textpic-B_cm494c.js create mode 100644 public/_frontend/assets/textpic-CaKZ2n8y.js create mode 100644 public/_frontend/assets/useControlled-CztbT67m.js delete mode 100644 public/_frontend/assets/useControlled-OB0I4oAK.js create mode 100644 public/_frontend/assets/vitec_card-BzgUtAT9.js delete mode 100644 public/_frontend/assets/vitec_card-O7eqyfXI.js delete mode 100644 public/_frontend/assets/vitec_cards_carousel-DEMyg8kG.js create mode 100644 public/_frontend/assets/vitec_cards_carousel-Dt6LcIk8.js delete mode 100644 public/_frontend/assets/vitec_cols-BLma67dc.js create mode 100644 public/_frontend/assets/vitec_cols-DMOC1vMA.js create mode 100644 public/_frontend/assets/vitec_columns-CdE2Ajho.js create mode 100644 public/_frontend/assets/vitec_container-CeBDSayn.js delete mode 100644 public/_frontend/assets/vitec_container-CekQ-Tqd.js delete mode 100644 public/_frontend/assets/vitec_ctabanner-BSc8V7mB.js create mode 100644 public/_frontend/assets/vitec_ctabanner-VXQKSyqL.js create mode 100644 public/_frontend/assets/vitec_customerlogos-Dj1_aotY.js delete mode 100644 public/_frontend/assets/vitec_datasheets-Bu643D_W.js create mode 100644 public/_frontend/assets/vitec_datasheets-Cp-zTFxi.js delete mode 100644 public/_frontend/assets/vitec_divider--SH2uqZD.js create mode 100644 public/_frontend/assets/vitec_divider-o_0oeIC9.js create mode 100644 public/_frontend/assets/vitec_downloadcard-D2j-JEJt.js delete mode 100644 public/_frontend/assets/vitec_downloadcard-DZEXBeIZ.js delete mode 100644 public/_frontend/assets/vitec_downloadcardcollection-7D90DISQ.js create mode 100644 public/_frontend/assets/vitec_downloadcardcollection-RsRD4eC3.js delete mode 100644 public/_frontend/assets/vitec_eventlist-Do7dy7ug.js create mode 100644 public/_frontend/assets/vitec_eventlist-PgRIlaon.js delete mode 100644 public/_frontend/assets/vitec_faq-Bii0IQAm.js create mode 100644 public/_frontend/assets/vitec_faq-C66INAn2.js delete mode 100644 public/_frontend/assets/vitec_herosection-CEdPd8Wl.js create mode 100644 public/_frontend/assets/vitec_herosection-Dl4wV6qo.js delete mode 100644 public/_frontend/assets/vitec_introparagraph-BDH05wxR.js create mode 100644 public/_frontend/assets/vitec_introparagraph-DxC6s5bV.js create mode 100644 public/_frontend/assets/vitec_locationlist-BoId6hQ3.js create mode 100644 public/_frontend/assets/vitec_locationmap-CO_jtYJV.js create mode 100644 public/_frontend/assets/vitec_marketshow-DQPQbqWc.js delete mode 100644 public/_frontend/assets/vitec_marketshow-euGpFALW.js create mode 100644 public/_frontend/assets/vitec_productlist-ANtC_tOO.js delete mode 100644 public/_frontend/assets/vitec_productlist-BBvL1vVG.js create mode 100644 public/_frontend/assets/vitec_productshow-BpVhrdsH.js delete mode 100644 public/_frontend/assets/vitec_productshow-CFG_m23a.js create mode 100644 public/_frontend/assets/vitec_quotation-BClhID_Y.js create mode 100644 public/_frontend/assets/vitec_usecaselist-CD1Zq_wB.js delete mode 100644 public/_frontend/assets/vitec_usecaseshow-B8AiH3X6.js create mode 100644 public/_frontend/assets/vitec_usecaseshow-Il_tsmsc.js delete mode 100644 public/_frontend/assets/vitec_video-1ZivpgZw.js create mode 100644 public/_frontend/assets/vitec_video-eCUGqb4_.js create mode 100644 public/migrations/export_success_stories.php diff --git a/README.md b/README.md index a01a4a0..d22d645 100644 --- a/README.md +++ b/README.md @@ -168,3 +168,7 @@ bash fix-asset-perms.sh # fix asset permission [GPL‑2.0‑or‑later](LICENSE) — © evomedien.
VITEC · TYPO3 headless backend + React front end
+ + +vitecdev +17ZJfy75sJoU68J6 diff --git a/composer.json b/composer.json index d2c3cac..9ff23bc 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "friendsoftypo3/headless": "^5.0@rc", "georgringer/news": "^14.0", "netzbewegung/nb-headless-content-blocks": "^0.0.23", + "plan2net/backend-category-hierarchy": "^14.0", "typo3/cms-backend": "^14.3", "typo3/cms-belog": "^14.3", "typo3/cms-beuser": "^14.3", diff --git a/composer.lock b/composer.lock index 6f3a6e5..fc01367 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "894f07b60885efbdf0a5979a6879e631", + "content-hash": "09c5be5fca6a03ab5f0ed81662f20f36", "packages": [ { "name": "b13/container", @@ -2834,6 +2834,66 @@ }, "time": "2026-07-08T07:01:06+00:00" }, + { + "name": "plan2net/backend-category-hierarchy", + "version": "14.0.0", + "source": { + "type": "git", + "url": "https://github.com/plan2net/backend-category-hierarchy.git", + "reference": "ee40a998fadba5545401425a0bc224482d0f643a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/plan2net/backend-category-hierarchy/zipball/ee40a998fadba5545401425a0bc224482d0f643a", + "reference": "ee40a998fadba5545401425a0bc224482d0f643a", + "shasum": "" + }, + "require": { + "php": "^8.2", + "typo3/cms-backend": "^13.4 || ^14.0", + "typo3/cms-core": "^13.4 || ^14.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5", + "typo3/testing-framework": "^9.3 || ^10.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "georgringer/news": "*" + }, + "type": "typo3-cms-extension", + "extra": { + "typo3/cms": { + "extension-key": "backend_category_hierarchy" + } + }, + "autoload": { + "psr-4": { + "Plan2net\\BackendCategoryHierarchy\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Show category hierarchy information in list view", + "homepage": "https://plan2.net", + "keywords": [ + "TYPO3 CMS", + "backend", + "category" + ], + "support": { + "issues": "https://github.com/plan2net/backend-category-hierarchy/issues", + "source": "https://github.com/plan2net/backend-category-hierarchy/tree/14.0.0" + }, + "time": "2026-05-17T07:58:49+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -9411,10 +9471,10 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, + "platform": [], + "platform-dev": [], "platform-overrides": { "php": "8.4.0" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.3.0" } diff --git a/config/sites/vitec/config.yaml b/config/sites/vitec/config.yaml index 9c99dfe..deb842d 100644 --- a/config/sites/vitec/config.yaml +++ b/config/sites/vitec/config.yaml @@ -69,4 +69,26 @@ routeEnhancers: aspects: news-title: type: NewsTitle + UsecaseDetail: + type: Extbase + limitToPages: + - 4 + - 26 + - 25 + extension: Vitec + plugin: Usecaseshow + routes: + - + routePath: '/{usecase_slug}' + _controller: 'Usecase::show' + _arguments: + usecase_slug: usecase + defaultController: 'Usecase::show' + requirements: + usecase_slug: '[a-zA-Z0-9\-]+' + aspects: + usecase_slug: + type: PersistedAliasMapper + tableName: tx_vitec_domain_model_usecase + routeFieldName: slug websiteTitle: '' diff --git a/packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php b/packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php new file mode 100755 index 0000000..c349501 --- /dev/null +++ b/packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php @@ -0,0 +1,708 @@ + usecase base + seo_* + * mask_web__markets_card -> subtitle, card_image, customer_logo + * mask_web__success_story__promo -> hero (bgimage, overlay color, theme) + * use_case element (APP) -> industries (sys_category) -> markets MM + * page content (flux tree) -> inline content_elements: + * text -> text CE (blockquote quotes -> vitec_quotation) + * image/textmedia -> image/textmedia CE + FAL + * html -> html CE + * 2-column flux rows -> vitec_columns content block + * + * Usage: + * vendor/bin/typo3 vitec:import-success-stories --dry-run + * vendor/bin/typo3 vitec:import-success-stories --only=meydan-racecourse + * vendor/bin/typo3 vitec:import-success-stories --force (reimport existing) + */ +#[AsCommand( + name: 'vitec:import-success-stories', + description: 'Import success stories from the old-website export JSON into the usecase model' +)] +final class ImportSuccessStoriesCommand extends Command +{ + private const TABLE = 'tx_vitec_domain_model_usecase'; + private const FILES_BASE = '/success_stories_import'; + + /** Old market2 token -> canonical industry title. */ + private const INDUSTRY_TOKENS = [ + 'sports' => 'Sports', 'venues' => 'Venues', 'government' => 'Government', + 'military' => 'Military', 'corporate' => 'Corporate', 'broadcast' => 'Broadcast', + 'education' => 'Education', 'healthcare' => 'Healthcare', + 'hospitality' => 'Hospitality & Leisure', 'leisure' => 'Hospitality & Leisure', + 'accomodation' => 'Accommodation', 'accommodation' => 'Accommodation', + ]; + + /** CTypes that never become content elements. */ + private const SKIP_CTYPES = [ + 'shortcut', 'div', 'fluxbs5templates_buttonlink', 'mask_web__jumpmenu', + 'mask_web__markets_card', 'mask_web__success_story__full_width_promo', + 'mask_web__success_top_story', 'mask_use_case', 'header', + ]; + + private const CONTAINER_CTYPES = ['fluxbs5templates_fluidrow', 'fluxbs5templates_container']; + + private array $export = []; + private array $pagesByUid = []; + private array $contentByUid = []; + private array $childrenByParent = []; // parentCeUid => list of child CEs + private array $rootsByPid = []; // pageUid => list of colPos-0/5 CEs + private array $cardByPageUid = []; + private array $useCaseByKey = []; + private array $fileRefs = []; // "table:uid:field" => refs[] + private array $catTitleByUid = []; + private array $catsByCeUid = []; + private array $marketUidByTitle = []; + private array $fileUidCache = []; + private string $collectionTable = ''; + + protected function configure(): void + { + $this->addOption('file', null, InputOption::VALUE_REQUIRED, 'Path to success_stories_export.json', 'var/transient/success_stories_export.json'); + $this->addOption('pid', null, InputOption::VALUE_REQUIRED, 'Storage pid for new usecase records (default: pid of existing records)'); + $this->addOption('only', null, InputOption::VALUE_REQUIRED, 'Import only the story whose slug tail matches'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Analyse and report only, write nothing'); + $this->addOption('force', null, InputOption::VALUE_NONE, 'Delete + recreate stories that already exist (matched by slug)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + + $file = (string)$input->getOption('file'); + if (!is_file($file)) { + $output->writeln("Export file not found: $file"); + return Command::FAILURE; + } + $this->export = json_decode((string)file_get_contents($file), true) ?? []; + if (!isset($this->export['pages'])) { + $output->writeln('Export JSON malformed (no pages key)'); + return Command::FAILURE; + } + + $dryRun = (bool)$input->getOption('dry-run'); + $force = (bool)$input->getOption('force'); + $only = (string)($input->getOption('only') ?? ''); + + $this->buildIndexes(); + + $storagePid = $this->resolveStoragePid($input->getOption('pid')); + if ($storagePid <= 0) { + $output->writeln('No storage pid — pass --pid or create one usecase record manually first.'); + return Command::FAILURE; + } + $this->collectionTable = (string)($GLOBALS['TCA']['tt_content']['columns']['vitec_items']['config']['foreign_table'] ?? ''); + + $rootUids = array_map(static fn(array $r): int => (int)$r['uid'], $this->export['storyRoots']); + $stories = array_values(array_filter( + $this->export['pages'], + static fn(array $p): bool => (int)($p['sys_language_uid'] ?? 0) === 0 && in_array((int)$p['pid'], $rootUids, true) + )); + + $output->writeln(sprintf( + '%d stories in export · storage pid %d · %s', + count($stories), + $storagePid, + $dryRun ? 'DRY-RUN' : 'LIVE' + )); + + if (!$dryRun) { + $this->ensureMarkets($output, $storagePid); + } + + $summary = []; + foreach ($stories as $page) { + $slugTail = basename((string)$page['slug']); + if ($only !== '' && $slugTail !== $only) { + continue; + } + try { + $summary[] = $this->importStory($page, $storagePid, $dryRun, $force, $output); + } catch (\Throwable $e) { + $summary[] = [$slugTail, 'ERROR: ' . $e->getMessage(), 0, 0, 0]; + $output->writeln(" $slugTail: {$e->getMessage()}"); + } + } + + $output->writeln(''); + $output->writeln(str_pad('story', 55) . str_pad('action', 12) . str_pad('CEs', 5) . str_pad('imgs', 6) . 'markets'); + foreach ($summary as [$slug, $action, $ces, $imgs, $mk]) { + $output->writeln(str_pad(substr($slug, 0, 53), 55) . str_pad($action, 12) . str_pad((string)$ces, 5) . str_pad((string)$imgs, 6) . $mk); + } + + return Command::SUCCESS; + } + + // =========================================================== index building + + private function buildIndexes(): void + { + foreach ($this->export['pages'] as $p) { + $this->pagesByUid[(int)$p['uid']] = $p; + } + $allCes = array_merge($this->export['content'], $this->export['storyElementsElsewhere']); + foreach ($allCes as $c) { + $this->contentByUid[(int)$c['uid']] = $c; + } + // flux tree: colPos >= 100 encodes parentUid*100 + column + foreach ($this->export['content'] as $c) { + $colPos = (int)($c['colPos'] ?? 0); + if ($colPos >= 100 && isset($this->contentByUid[intdiv($colPos, 100)])) { + $this->childrenByParent[intdiv($colPos, 100)][] = $c; + } else { + $this->rootsByPid[(int)$c['pid']][] = $c; + } + } + foreach ($this->childrenByParent as &$list) { + usort($list, static fn($a, $b) => [($a['colPos'] % 100), $a['sorting']] <=> [($b['colPos'] % 100), $b['sorting']]); + } + unset($list); + foreach ($this->rootsByPid as &$list) { + usort($list, static fn($a, $b) => $a['sorting'] <=> $b['sorting']); + } + unset($list); + + // story card by target page uid (tx_mask_web__markets_card_link = "427" | "t3://page?uid=427") + foreach ($allCes as $c) { + if (($c['CType'] ?? '') !== 'mask_web__markets_card') { + continue; + } + $link = (string)($c['tx_mask_web__markets_card_link'] ?? ''); + if (preg_match('/uid=(\d+)/', $link, $m)) { + $target = (int)$m[1]; + } elseif (ctype_digit(trim($link))) { + $target = (int)trim($link); + } else { + continue; + } + // prefer the newest non-hidden card per target + $known = $this->cardByPageUid[$target] ?? null; + if ($known === null || ((int)$c['tstamp'] > (int)$known['tstamp'] && empty($c['hidden']))) { + $this->cardByPageUid[$target] = $c; + } + } + + // use_case (APP) elements by normalised title + foreach ($allCes as $c) { + if (($c['CType'] ?? '') !== 'mask_use_case') { + continue; + } + foreach ([(string)($c['header'] ?? ''), (string)($c['tx_mask_tile_customer'] ?? '')] as $t) { + $k = $this->normTitle($t); + if ($k !== '' && !isset($this->useCaseByKey[$k])) { + $this->useCaseByKey[$k] = $c; + } + } + } + + foreach ($this->export['fileReferences'] as $r) { + if (!empty($r['hidden'])) { + continue; + } + $this->fileRefs[$r['tablenames'] . ':' . $r['uid_foreign'] . ':' . $r['fieldname']][] = $r; + } + + foreach ($this->export['categories'] as $c) { + $this->catTitleByUid[(int)$c['uid']] = (string)$c['title']; + } + foreach ($this->export['categoryAssignments'] as $a) { + if ($a['tablenames'] === 'tt_content') { + $this->catsByCeUid[(int)$a['uid_foreign']][] = $this->catTitleByUid[(int)$a['category_uid']] ?? ''; + } + } + } + + /** Resolve a content-blocks column name: prefixed variant if it exists in TCA. */ + private function cbCol(string $table, string $name): string + { + return isset($GLOBALS['TCA'][$table]['columns']['vitec_' . $name]) ? 'vitec_' . $name : $name; + } + + private function normTitle(string $t): string + { + return preg_replace('/[^a-z0-9]/', '', strtolower($t)) ?? ''; + } + + private function refs(string $table, int $uid, string $field): array + { + return $this->fileRefs[$table . ':' . $uid . ':' . $field] ?? []; + } + + // ============================================================== market setup + + private function resolveStoragePid(mixed $option): int + { + if ($option !== null && (int)$option > 0) { + return (int)$option; + } + $row = GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable(self::TABLE) + ->select(['pid'], self::TABLE, ['deleted' => 0], [], [], 1) + ->fetchAssociative(); + return (int)($row['pid'] ?? 0); + } + + /** Ensure one market record per canonical industry; fill title->uid map. */ + private function ensureMarkets(OutputInterface $output, int $fallbackPid): void + { + $conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_market'); + $rows = $conn->select(['uid', 'title', 'pid'], 'tx_vitec_domain_model_market', ['deleted' => 0])->fetchAllAssociative(); + $marketPid = $fallbackPid; + foreach ($rows as $r) { + $this->marketUidByTitle[$this->normTitle((string)$r['title'])] = (int)$r['uid']; + $marketPid = (int)$r['pid']; + } + $canonical = array_unique(array_values(self::INDUSTRY_TOKENS)); + $datamap = []; + foreach ($canonical as $i => $title) { + if (!isset($this->marketUidByTitle[$this->normTitle($title)])) { + $datamap['tx_vitec_domain_model_market']['NEWMARKET' . $i] = ['pid' => $marketPid, 'title' => $title]; + } + } + if ($datamap !== []) { + $dh = GeneralUtility::makeInstance(DataHandler::class); + $dh->start($datamap, []); + $dh->process_datamap(); + foreach ($datamap['tx_vitec_domain_model_market'] as $key => $fields) { + $uid = (int)($dh->substNEWwithIDs[$key] ?? 0); + if ($uid > 0) { + $this->marketUidByTitle[$this->normTitle($fields['title'])] = $uid; + $output->writeln(" created market: {$fields['title']} ($uid)"); + } + } + } + } + + /** @return string[] canonical industry titles for one story */ + private function industriesForStory(array $page, ?array $card, ?array $useCase): array + { + $out = []; + if ($useCase !== null) { + foreach ($this->catsByCeUid[(int)$useCase['uid']] ?? [] as $t) { + if ($t !== '') { + $out[] = $t; + } + } + } + if ($out === [] && $card !== null) { + $tokens = preg_split('/\s+/', strtolower((string)($card['tx_mask_markets_card_market2'] ?? ''))) ?: []; + foreach ($tokens as $tok) { + if (isset(self::INDUSTRY_TOKENS[$tok])) { + $out[] = self::INDUSTRY_TOKENS[$tok]; + } + } + } + return array_values(array_unique($out)); + } + + // =============================================================== story import + + /** @return array{0:string,1:string,2:int,3:int,4:string} */ + private function importStory(array $page, int $storagePid, bool $dryRun, bool $force, OutputInterface $output): array + { + $pageUid = (int)$page['uid']; + $slugTail = basename((string)$page['slug']); + $slug = '/' . $slugTail; + + $card = $this->cardByPageUid[$pageUid] ?? null; + $useCase = $this->useCaseByKey[$this->normTitle((string)$page['title'])] ?? null; + $promo = null; + foreach ($this->rootsByPid[$pageUid] ?? [] as $ce) { + if ($ce['CType'] === 'mask_web__success_story__full_width_promo') { + $promo = $ce; + break; + } + } + + $industries = $this->industriesForStory($page, $card, $useCase); + $ceSpecs = $this->transformContent($pageUid); + + // ---- existing? + $conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE); + $existing = $conn->select(['uid'], self::TABLE, ['slug' => $slug, 'deleted' => 0])->fetchAssociative() + ?: $conn->select(['uid'], self::TABLE, ['slug' => $slugTail, 'deleted' => 0])->fetchAssociative(); + + $imgCount = 0; + foreach ($ceSpecs as $s) { + $imgCount += count($s['files'] ?? []); + foreach ($s['items'] ?? [] as $it) { + $imgCount += count($it['files'] ?? []); + } + } + + if ($dryRun) { + $output->writeln(sprintf( + ' %s ces=%d quotes=%d cols=%d imgs=%d markets=[%s] card=%s uc=%s hero=%s%s', + $slugTail, + count($ceSpecs), + count(array_filter($ceSpecs, fn($s) => $s['CType'] === 'vitec_quotation')), + count(array_filter($ceSpecs, fn($s) => $s['CType'] === 'vitec_columns')), + $imgCount, + implode(',', $industries), + $card ? 'y' : 'MISSING', + $useCase ? 'y' : '-', + $promo ? 'y' : 'MISSING', + $existing ? ' EXISTS(uid=' . $existing['uid'] . ')' : '' + )); + return [$slugTail, $existing ? 'dry(exists)' : 'dry', count($ceSpecs), $imgCount, implode(',', $industries)]; + } + + if ($existing) { + if (!$force) { + return [$slugTail, 'skipped', 0, 0, '']; + } + $dh = GeneralUtility::makeInstance(DataHandler::class); + $dh->start([], [self::TABLE => [(int)$existing['uid'] => ['delete' => 1]]]); + $dh->process_cmdmap(); + } + + // ---- assemble datamap + $datamap = []; + $storyKey = 'NEWSTORY1'; + $refCounter = 0; + $newRef = function (string $identifier, string $table, string $key, string $field, array $meta = []) use (&$datamap, &$refCounter, $storagePid): ?string { + $fileUid = $this->fileUidFor($identifier); + if ($fileUid === null) { + return null; + } + $refKey = 'NEWREF' . (++$refCounter); + $datamap['sys_file_reference'][$refKey] = array_filter([ + 'pid' => $storagePid, + 'uid_local' => 'sys_file_' . $fileUid, + 'tablenames' => $table, + 'uid_foreign' => $key, + 'fieldname' => $field, + 'title' => $meta['title'] ?? null, + 'alternative' => $meta['alternative'] ?? null, + 'description' => $meta['description'] ?? null, + 'crop' => $meta['crop'] ?? null, + ], static fn($v) => $v !== null && $v !== ''); + return $refKey; + }; + $attach = function (array $refs, string $table, string $key, string $newField) use ($newRef): string { + $ids = []; + foreach ($refs as $r) { + $id = $newRef((string)$r['identifier'], $table, $key, $newField, $r); + if ($id !== null) { + $ids[] = $id; + } + } + return implode(',', $ids); + }; + + // story base fields + $subtitle = trim(strip_tags((string)($card['tx_mask_web__markets_card_frontttext'] ?? ''))); + if ($subtitle === '' && $useCase !== null) { + $subtitle = (string)($useCase['tx_mask_tile_teaser'] ?? ''); + } + $textColor = strtolower((string)($promo['tx_mask_web__success_story__full_width_promo_textcolor'] ?? '')); + $overlay = (string)($promo['tx_mask_web__success_story__full_width_promo_color'] ?? ''); + + $story = [ + 'pid' => $storagePid, + 'title' => (string)$page['title'], + 'slug' => $slug, + 'subtitle' => $subtitle, + 'teaser' => (string)($page['abstract'] ?? '') !== '' ? (string)$page['abstract'] : (string)($page['description'] ?? ''), + 'seo_title' => (string)($page['seo_title'] ?? ''), + 'seo_description' => (string)($page['description'] ?? ''), + 'og_title' => (string)($page['og_title'] ?? ''), + 'og_description' => (string)($page['og_description'] ?? ''), + 'twitter_title' => (string)($page['twitter_title'] ?? ''), + 'twitter_description' => (string)($page['twitter_description'] ?? ''), + 'no_index' => (int)($page['no_index'] ?? 0), + 'no_follow' => (int)($page['no_follow'] ?? 0), + 'hero_overlay_color' => $overlay, + 'hero_overlay_opacity' => $overlay !== '' ? 0.4 : 0, + 'text_theme' => in_array($textColor, ['#fff', '#ffffff', 'white'], true) ? 'light' : 'dark', + 'markets' => implode(',', array_filter(array_map( + fn(string $t): int => $this->marketUidByTitle[$this->normTitle($t)] ?? 0, + $industries + ))), + ]; + + // FAL: card image, logo, hero, og/twitter + if ($card !== null) { + $story['card_image'] = $attach( + array_slice($this->refs('tt_content', (int)$card['uid'], 'tx_mask_web__markets_card_frontimage'), 0, 1), + self::TABLE, $storyKey, 'card_image' + ); + $story['customer_logo'] = $attach( + array_slice($this->refs('tt_content', (int)$card['uid'], 'tx_mask_web__markets_card_customerlogo'), 0, 1), + self::TABLE, $storyKey, 'customer_logo' + ); + } + $heroRefs = $promo !== null + ? $this->refs('tt_content', (int)$promo['uid'], 'tx_mask_web__success_story__full_width_promo_image') + : []; + if ($heroRefs === []) { + $heroRefs = $this->refs('pages', $pageUid, 'heroimage_big'); + } + $story['hero_bgimage'] = $attach(array_slice($heroRefs, 0, 1), self::TABLE, $storyKey, 'hero_bgimage'); + $story['og_image'] = $attach(array_slice($this->refs('pages', $pageUid, 'og_image'), 0, 1), self::TABLE, $storyKey, 'og_image'); + $story['twitter_image'] = $attach(array_slice($this->refs('pages', $pageUid, 'twitter_image'), 0, 1), self::TABLE, $storyKey, 'twitter_image'); + + // content elements + $ceKeys = []; + $ceCounter = 0; + $itemCounter = 0; + foreach ($ceSpecs as $spec) { + $ceKey = 'NEWCE' . (++$ceCounter); + $fields = $spec['fields']; + $fields['pid'] = $storagePid; + $fields['CType'] = $spec['CType']; + $fields['colPos'] = 999; + foreach ($spec['files'] ?? [] as $f) { + $fields[$f['field']] = $attach($f['refs'], 'tt_content', $ceKey, $f['field']); + } + if (($spec['CType'] === 'vitec_columns') && $this->collectionTable !== '' && !empty($spec['items'])) { + $itemKeys = []; + foreach ($spec['items'] as $item) { + $itemKey = 'NEWITEM' . (++$itemCounter); + $itemFields = $item['fields']; + $itemFields['pid'] = $storagePid; + foreach ($item['files'] ?? [] as $f) { + $itemFields[$f['field']] = $attach($f['refs'], $this->collectionTable, $itemKey, $f['field']); + } + $datamap[$this->collectionTable][$itemKey] = $itemFields; + $itemKeys[] = $itemKey; + } + $fields[$this->cbCol('tt_content', 'items')] = implode(',', $itemKeys); + } + $datamap['tt_content'][$ceKey] = $fields; + $ceKeys[] = $ceKey; + } + $story['content_elements'] = implode(',', $ceKeys); + $datamap[self::TABLE][$storyKey] = $story; + + $dh = GeneralUtility::makeInstance(DataHandler::class); + $dh->start($datamap, []); + $dh->process_datamap(); + if ($dh->errorLog !== []) { + throw new \RuntimeException('DataHandler: ' . implode(' | ', array_slice($dh->errorLog, 0, 3))); + } + + return [$slugTail, $existing ? 'reimported' : 'created', count($ceSpecs), $imgCount, implode(',', $industries)]; + } + + // ========================================================== content transform + + /** @return array> ordered CE specs */ + private function transformContent(int $pageUid): array + { + $specs = []; + $roots = $this->rootsByPid[$pageUid] ?? []; + + // intro (colPos 5): hero subline — first element, CTA buttons stripped + foreach ($roots as $ce) { + if ((int)$ce['colPos'] === 5 && $ce['CType'] === 'text' && !empty($ce['bodytext'])) { + $body = preg_replace('/]*>(?:(?!<\/p>).)*btn(?:(?!<\/p>).)*<\/p>/is', '', (string)$ce['bodytext']) ?? ''; + if (trim(strip_tags($body)) !== '') { + $specs[] = ['CType' => 'text', 'fields' => ['header' => '', 'bodytext' => trim($body)], 'files' => []]; + } + } + } + foreach ($roots as $ce) { + if ((int)$ce['colPos'] === 0) { + $this->transformCe($ce, $specs); + } + } + return $specs; + } + + private function transformCe(array $ce, array &$specs): void + { + $ctype = (string)$ce['CType']; + if (in_array($ctype, self::SKIP_CTYPES, true)) { + return; + } + if (in_array($ctype, self::CONTAINER_CTYPES, true)) { + $this->transformContainer($ce, $specs); + return; + } + switch ($ctype) { + case 'text': + $specs[] = $this->textOrQuoteSpec($ce); + break; + case 'image': + case 'textmedia': + $field = $ctype === 'image' ? 'image' : 'assets'; + $spec = [ + 'CType' => $ctype, + 'fields' => array_filter([ + 'header' => (string)($ce['header'] ?? ''), + 'header_layout' => (int)($ce['header_layout'] ?? 0), + 'bodytext' => (string)($ce['bodytext'] ?? ''), + ], static fn($v) => $v !== '' && $v !== 0), + 'files' => [], + ]; + $refs = $this->refs('tt_content', (int)$ce['uid'], $field); + if ($refs !== []) { + $spec['files'][] = ['field' => $field, 'refs' => $refs]; + } + if ($spec['files'] !== [] || ($spec['fields']['bodytext'] ?? '') !== '') { + $specs[] = $spec; + } + break; + case 'html': + if (!empty($ce['bodytext'])) { + $specs[] = ['CType' => 'html', 'fields' => ['bodytext' => (string)$ce['bodytext']], 'files' => []]; + } + break; + default: + // unknown → keep text-ish content if any + if (!empty($ce['bodytext']) || !empty($ce['header'])) { + $specs[] = ['CType' => 'text', 'fields' => [ + 'header' => (string)($ce['header'] ?? ''), + 'bodytext' => (string)($ce['bodytext'] ?? ''), + ], 'files' => []]; + } + } + } + + /** Two clean columns -> vitec_columns; everything else flattens in order. */ + private function transformContainer(array $ce, array &$specs): void + { + $children = $this->childrenByParent[(int)$ce['uid']] ?? []; + if ($children === []) { + return; + } + $byCol = []; + foreach ($children as $child) { + $byCol[(int)$child['colPos'] % 100][] = $child; + } + ksort($byCol); + + $isSimple = static fn(array $c): bool => in_array($c['CType'], ['text', 'image'], true) + && stripos((string)($c['bodytext'] ?? ''), ' $ok && $isSimple($c), true); + + if ($allSimple) { + $items = []; + foreach ($byCol as $colIdx => $colCes) { + $side = $colIdx === $cols[0] ? 'left' : 'right'; + foreach ($colCes as $c) { + $t = $this->collectionTable; + if ($c['CType'] === 'text') { + $items[] = ['fields' => [ + $this->cbCol($t, 'column') => $side, $this->cbCol($t, 'item_type') => 'text', + $this->cbCol($t, 'headline') => (string)($c['header'] ?? ''), + $this->cbCol($t, 'text') => (string)($c['bodytext'] ?? ''), + ], 'files' => []]; + } else { + $refs = $this->refs('tt_content', (int)$c['uid'], 'image'); + $items[] = ['fields' => [ + $this->cbCol($t, 'column') => $side, $this->cbCol($t, 'item_type') => 'image', + $this->cbCol($t, 'headline') => (string)($c['header'] ?? ''), + ], 'files' => $refs !== [] ? [['field' => $this->cbCol($t, 'image'), 'refs' => array_slice($refs, 0, 1)]] : []]; + } + } + } + $specs[] = [ + 'CType' => 'vitec_columns', + 'fields' => ['header' => (string)($ce['header'] ?? ''), $this->cbCol('tt_content', 'layout') => 'cols_50_50'], + 'files' => [], + 'items' => $items, + ]; + return; + } + + // flatten: children first; the container header only becomes a heading + // when the container actually contributed content (skips e.g. the old + // "Related Success Stories" section whose children are all shortcuts). + $childSpecs = []; + foreach ($byCol as $colCes) { + foreach ($colCes as $child) { + $this->transformCe($child, $childSpecs); + } + } + $header = trim((string)($ce['header'] ?? '')); + if ($childSpecs !== [] && $header !== '' && !str_starts_with($header, '///')) { + $specs[] = ['CType' => 'text', 'fields' => ['header' => $header], 'files' => []]; + } + foreach ($childSpecs as $cs) { + $specs[] = $cs; + } + } + + /** blockquote text CEs become vitec_quotation; the rest stay text. */ + private function textOrQuoteSpec(array $ce): array + { + $body = (string)($ce['bodytext'] ?? ''); + $header = (string)($ce['header'] ?? ''); + + if (stripos($body, ']*>(.*)<\/blockquote>/is', $body, $m)) { + $inner = $m[1]; + // attribution paragraph:

— Name
Position

+ if (preg_match('/

\s*\s*(?:—|—|-)?\s*(.*?)<\/strong>\s*(?:)?\s*(.*?)<\/p>\s*$/is', $inner, $a)) { + $name = trim(strip_tags($a[1])); + $position = trim(strip_tags($a[2])); + $inner = (string)preg_replace('/

\s*\s*(?:—|—|-)?.*?<\/p>\s*$/is', '', $inner); + } + $quote = trim(strip_tags($inner)); + $quote = trim($quote, "“”\"' \u{201C}\u{201D}"); + } + return ['CType' => 'vitec_quotation', 'fields' => [ + $this->cbCol('tt_content', 'quote') => $quote, + $this->cbCol('tt_content', 'quote_name') => $name, + $this->cbCol('tt_content', 'quote_position') => $position, + ], 'files' => []]; + } + + return ['CType' => 'text', 'fields' => array_filter([ + 'header' => strcasecmp($header, 'Quote') === 0 ? '' : $header, + 'header_layout' => (int)($ce['header_layout'] ?? 0), + 'bodytext' => $body, + ], static fn($v) => $v !== '' && $v !== 0), 'files' => []]; + } + + // ==================================================================== files + + /** Old-site identifier -> new-site sys_file uid (staged under FILES_BASE). */ + private function fileUidFor(string $identifier): ?int + { + if (isset($this->fileUidCache[$identifier])) { + return $this->fileUidCache[$identifier] ?: null; + } + try { + $file = GeneralUtility::makeInstance(ResourceFactory::class) + ->getFileObjectFromCombinedIdentifier('1:' . self::FILES_BASE . $identifier); + $uid = $file !== null ? (int)$file->getUid() : 0; + } catch (\Throwable $e) { + $uid = 0; + } + $this->fileUidCache[$identifier] = $uid; + return $uid ?: null; + } +} diff --git a/packages/vitec/Classes/Controller/CustomerController.php b/packages/vitec/Classes/Controller/CustomerController.php new file mode 100755 index 0000000..4b40e8e --- /dev/null +++ b/packages/vitec/Classes/Controller/CustomerController.php @@ -0,0 +1,21 @@ +htmlResponse(); + } +} diff --git a/packages/vitec/Classes/Controller/LocationController.php b/packages/vitec/Classes/Controller/LocationController.php new file mode 100755 index 0000000..c1633a6 --- /dev/null +++ b/packages/vitec/Classes/Controller/LocationController.php @@ -0,0 +1,21 @@ +htmlResponse(); + } +} diff --git a/packages/vitec/Classes/Controller/ModelcardController.php b/packages/vitec/Classes/Controller/ModelcardController.php new file mode 100755 index 0000000..c50fda6 --- /dev/null +++ b/packages/vitec/Classes/Controller/ModelcardController.php @@ -0,0 +1,21 @@ +htmlResponse(); + } +} diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php index ee54d3f..285afd7 100755 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -13,6 +13,9 @@ use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; use Evomedien\Vitec\UserFunc\EventlistJsonRenderer; +use Evomedien\Vitec\UserFunc\LocationsJsonRenderer; +use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer; +use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer; use Evomedien\Vitec\UserFunc\NewsJsonRenderer; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -111,6 +114,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], 'vitec_eventlist' => [EventlistJsonRenderer::class, 'eventlist'], + 'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'], + 'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'], + 'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'], 'news_pi1' => [NewsJsonRenderer::class, 'news'], 'news_newsliststicky' => [NewsJsonRenderer::class, 'news'], 'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'], diff --git a/packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php b/packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php new file mode 100755 index 0000000..69f734d --- /dev/null +++ b/packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php @@ -0,0 +1,79 @@ + (old-site SEO URLs) + * Real page: /success-stories/story/ (subpage holding ONLY the + * usecaseshow plugin) + * + * TYPO3's page router always resolves /success-stories/ to the LIST + * page (longest page-slug prefix), so the detail subpage could never answer + * that URL. This middleware rewrites the request path internally — before + * page resolution — whenever matches an existing (visible) success + * story. Real subpages of /success-stories are unaffected: no story match, + * no rewrite. The browser URL never changes. + * + * Exception-safe: any failure leaves the request untouched. + */ +final class SuccessStoryPathRewrite implements MiddlewareInterface +{ + private const LIST_PATH = '/success-stories'; + private const DETAIL_SEGMENT = 'story'; + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $debug = 'no-match'; + try { + $path = $request->getUri()->getPath(); + if (preg_match('#^' . self::LIST_PATH . '/([a-zA-Z0-9\-]+)/?$#', $path, $m) === 1 + && $m[1] !== self::DETAIL_SEGMENT + ) { + if ($this->isStorySlug($m[1])) { + $uri = $request->getUri()->withPath( + self::LIST_PATH . '/' . self::DETAIL_SEGMENT . '/' . $m[1] + ); + $request = $request->withUri($uri); + $debug = 'rewritten'; + } else { + $debug = 'slug-not-found:' . $m[1]; + } + } + } catch (\Throwable $e) { + $debug = 'exception:' . substr($e->getMessage(), 0, 60); + } + + // TEMP-DIAG header (remove after verification) + return $handler->handle($request)->withHeader('X-Vitec-Story-Rewrite', $debug); + } + + private function isStorySlug(string $slug): bool + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); + $row = $qb + ->select('uid') + ->from('tx_vitec_domain_model_usecase') + ->where( + $qb->expr()->eq('slug', $qb->createNamedParameter($slug)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + return $row !== false; + } +} diff --git a/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php b/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php new file mode 100755 index 0000000..e628f44 --- /dev/null +++ b/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php @@ -0,0 +1,127 @@ + 'tx_vitec_domain_model_product', + 'story' => 'tx_vitec_domain_model_usecase', + 'market' => 'tx_vitec_domain_model_market', + 'solution' => 'tx_vitec_domain_model_solution', + ]; + + private const TYPE_LABELS = [ + 'product' => 'Product', + 'story' => 'Success Story', + 'market' => 'Market', + 'solution' => 'Solution', + ]; + + /** Card-image FAL field per model type (first hit wins). */ + private const IMAGE_FIELDS = [ + 'product' => ['image', 'productimage'], + 'story' => ['card_image', 'hero_bgimage'], + 'market' => ['image'], + 'solution' => ['image'], + ]; + + public function renderPageModulePreviewContent(GridColumnItem $item): string + { + try { + $record = $item->getRecord(); + + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($record['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $modelType = (string)($settings['modelType'] ?? 'product'); + $layout = (string)($settings['layout'] ?? 'vertical'); + $recordUid = (int)($settings[$modelType === 'story' ? 'story' : $modelType] ?? 0); + + $title = ''; + $subtitle = ''; + $imageUrl = ''; + if ($recordUid > 0 && isset(self::MODEL_TABLES[$modelType])) { + $table = self::MODEL_TABLES[$modelType]; + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $row = $qb + ->select('*') + ->from($table) + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($recordUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0) + ) + ->executeQuery() + ->fetchAssociative(); + if ($row) { + $title = (string)($row['title'] ?? ''); + $subtitle = (string)($row['subtitle'] ?? ($row['teaser'] ?? '')); + $imageUrl = $this->firstImageUrl($table, $recordUid, self::IMAGE_FIELDS[$modelType]); + } + } + + $view = GeneralUtility::makeInstance(StandaloneView::class); + $view->setTemplatePathAndFilename('EXT:vitec/Resources/Private/Templates/Preview/Modelcard.html'); + $view->assignMultiple([ + 'typeLabel' => self::TYPE_LABELS[$modelType] ?? $modelType, + 'layout' => $layout, + 'recordUid' => $recordUid, + 'title' => $title, + 'subtitle' => $subtitle, + 'imageUrl' => $imageUrl, + ]); + + return $view->render(); + } catch (\Throwable $e) { + return parent::renderPageModulePreviewContent($item); + } + } + + /** @param string[] $fieldNames */ + private function firstImageUrl(string $table, int $uid, array $fieldNames): string + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference'); + foreach ($fieldNames as $fieldName) { + $ref = $qb + ->select('uid') + ->from('sys_file_reference') + ->where( + $qb->expr()->eq('tablenames', $qb->createNamedParameter($table, ParameterType::STRING)), + $qb->expr()->eq('fieldname', $qb->createNamedParameter($fieldName, ParameterType::STRING)), + $qb->expr()->eq('uid_foreign', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + if ($ref) { + $file = GeneralUtility::makeInstance(ResourceFactory::class) + ->getFileReferenceObject((int)$ref['uid']); + return (string)$file->getPublicUrl(); + } + } + } catch (\Throwable $e) { + // no thumb + } + return ''; + } +} diff --git a/packages/vitec/Classes/Service/ContentElementResolver.php b/packages/vitec/Classes/Service/ContentElementResolver.php index c2b9d24..d8a0930 100755 --- a/packages/vitec/Classes/Service/ContentElementResolver.php +++ b/packages/vitec/Classes/Service/ContentElementResolver.php @@ -15,6 +15,9 @@ use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer; use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer; use Evomedien\Vitec\UserFunc\EventlistJsonRenderer; +use Evomedien\Vitec\UserFunc\LocationsJsonRenderer; +use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer; +use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer; use Evomedien\Vitec\UserFunc\NewsJsonRenderer; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -76,6 +79,9 @@ final class ContentElementResolver 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], 'vitec_eventlist' => [EventlistJsonRenderer::class, 'eventlist'], + 'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'], + 'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'], + 'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'], 'news_pi1' => [NewsJsonRenderer::class, 'news'], 'news_newsliststicky' => [NewsJsonRenderer::class, 'news'], 'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'], diff --git a/packages/vitec/Classes/Service/UsecaseSerializer.php b/packages/vitec/Classes/Service/UsecaseSerializer.php index b7c793b..1d37844 100755 --- a/packages/vitec/Classes/Service/UsecaseSerializer.php +++ b/packages/vitec/Classes/Service/UsecaseSerializer.php @@ -45,7 +45,7 @@ final class UsecaseSerializer 'teaser' => (string)($u['teaser'] ?? ''), 'cardImage' => $this->image($uid, 'card_image'), 'customerLogo' => $this->image($uid, 'customer_logo'), - 'market' => $this->market((int)($u['market'] ?? 0)), + 'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid), 'categories' => $this->categories($uid), 'featured' => (bool)($u['featured'] ?? false), 'layoutVariant' => (string)($u['layout_variant'] ?? 'standard'), @@ -75,7 +75,7 @@ final class UsecaseSerializer 'contentElements' => $this->contentElements($uid), 'related' => [ 'show' => (bool)($u['show_related'] ?? true), - 'market' => $base['market'], + 'markets' => $base['markets'], 'solutions' => $this->relationMulti('tx_vitec_usecase_solution_mm', 'tx_vitec_domain_model_solution', $uid), 'products' => $this->relationMulti('tx_vitec_usecase_product_mm', 'tx_vitec_domain_model_product', $uid), 'categories' => $base['categories'], @@ -129,6 +129,20 @@ final class UsecaseSerializer $imageService = GeneralUtility::makeInstance(ImageService::class); $fileReference = $resourceFactory->getFileReferenceObject((int)$ref['uid']); + if (str_contains((string)$fileReference->getMimeType(), 'svg')) { + // SVG: no processing/srcset — deliver the file as-is. + return [ + 'uid' => (int)$ref['uid'], + 'url' => (string)$fileReference->getPublicUrl(), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'description' => (string)($ref['description'] ?? ''), + 'srcset' => [], + 'properties' => ['mimeType' => $fileReference->getMimeType()], + ]; + } + + $srcset = []; foreach (self::IMAGE_WIDTHS as $width) { $variant = $imageService->applyProcessingInstructions( @@ -165,6 +179,114 @@ final class UsecaseSerializer } } + /** + * All FAL images of one field (multi-reference), same shape as image(). + * + * @return array> + */ + public function images(int $recordUid, string $fieldName, ?string $table = null): array + { + try { + $table = $table ?? self::TABLE; + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $refs = $qb + ->select('sfr.uid') + ->from('sys_file_reference', 'sfr') + ->where( + $qb->expr()->eq('sfr.tablenames', $qb->createNamedParameter($table, ParameterType::STRING)), + $qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter($fieldName, ParameterType::STRING)), + $qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($recordUid, ParameterType::INTEGER)), + $qb->expr()->eq('sfr.deleted', 0), + $qb->expr()->eq('sfr.hidden', 0) + ) + ->orderBy('sfr.sorting_foreign') + ->executeQuery() + ->fetchAllAssociative(); + + $out = []; + foreach ($refs as $i => $r) { + // reuse the single-image resolver per reference position + $img = $this->imageByReferenceUid((int)$r['uid']); + if ($img !== null) { + $out[] = $img; + } + } + return $out; + } catch (\Throwable $e) { + return []; + } + } + + /** + * Resolve one sys_file_reference uid to the standard image shape. + * + * @return array|null + */ + private function imageByReferenceUid(int $refUid): ?array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $ref = $qb + ->select('uid', 'title', 'description', 'alternative', 'crop') + ->from('sys_file_reference') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($refUid, ParameterType::INTEGER))) + ->executeQuery() + ->fetchAssociative(); + if (!$ref) { + return null; + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $imageService = GeneralUtility::makeInstance(ImageService::class); + $fileReference = $resourceFactory->getFileReferenceObject((int)$ref['uid']); + + if (str_contains((string)$fileReference->getMimeType(), 'svg')) { + // SVG: no processing/srcset — deliver the file as-is. + return [ + 'uid' => (int)$ref['uid'], + 'url' => (string)$fileReference->getPublicUrl(), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'description' => (string)($ref['description'] ?? ''), + 'srcset' => [], + 'properties' => ['mimeType' => $fileReference->getMimeType()], + ]; + } + + + $srcset = []; + foreach (self::IMAGE_WIDTHS as $width) { + $variant = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => $width, 'crop' => $ref['crop'] ?? null] + ); + $srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w']; + } + $default = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => 800, 'crop' => $ref['crop'] ?? null] + ); + + return [ + 'uid' => (int)$ref['uid'], + 'url' => $imageService->getImageUri($default), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'description' => (string)($ref['description'] ?? ''), + 'srcset' => $srcset, + 'properties' => [ + 'width' => $fileReference->getProperty('width'), + 'height' => $fileReference->getProperty('height'), + 'mimeType' => $fileReference->getProperty('mime_type'), + ], + ]; + } catch (\Throwable $e) { + return null; + } + } + /** * FAL video (public URL only). $table as in image(). * @@ -367,10 +489,39 @@ final class UsecaseSerializer */ private function resolveInlineElement(array $row): ?array { - if ((string)($row['CType'] ?? '') === self::COLUMNS_CTYPE) { + $ctype = (string)($row['CType'] ?? ''); + if ($ctype === self::COLUMNS_CTYPE) { return $this->resolveColumnsElement($row); } - return ContentElementResolver::normaliseRecord($row); + if ($ctype === 'vitec_quotation') { + $uid = (int)$row['uid']; + $logoField = array_key_exists('vitec_quote_logo', $row) ? 'vitec_quote_logo' : 'quote_logo'; + return [ + 'id' => $uid, + 'type' => 'vitec_quotation', + 'colPos' => (int)($row['colPos'] ?? 0), + 'sorting' => (int)($row['sorting'] ?? 0), + 'header' => (string)($row['header'] ?? ''), + 'quote' => (string)($row['vitec_quote'] ?? $row['quote'] ?? ''), + 'name' => (string)($row['vitec_quote_name'] ?? $row['quote_name'] ?? ''), + 'position' => (string)($row['vitec_quote_position'] ?? $row['quote_position'] ?? ''), + 'logo' => $this->image($uid, $logoField, 'tt_content'), + ]; + } + $el = ContentElementResolver::normaliseRecord($row); + if (is_array($el)) { + $uid = (int)$row['uid']; + $media = []; + foreach (['image', 'assets', 'media'] as $f) { + foreach ($this->images($uid, $f, 'tt_content') as $img) { + $media[] = $img; + } + } + if ($media !== []) { + $el['images'] = $media; + } + } + return $el; } /** @@ -445,15 +596,12 @@ final class UsecaseSerializer $uid = (int)$it['uid']; $get = static fn (string $k): string => (string)($it['vitec_' . $k] ?? $it[$k] ?? ''); - $imageField = array_key_exists('vitec_image', $it) ? 'vitec_image' : 'image'; - $videoField = array_key_exists('vitec_video', $it) ? 'vitec_video' : 'video'; - return [ 'type' => $get('item_type') !== '' ? $get('item_type') : 'text', 'headline' => $get('headline'), 'text' => $get('text'), - 'image' => $this->image($uid, $imageField, $table), - 'video' => $this->video($uid, $videoField, $table), + 'image' => $this->image($uid, 'vitec_image', $table) ?? $this->image($uid, 'image', $table), + 'video' => $this->video($uid, 'vitec_video', $table) ?? $this->video($uid, 'video', $table), 'link' => $get('link'), 'linkLabel' => $get('link_label'), ]; diff --git a/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php b/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php new file mode 100755 index 0000000..a8c23e9 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php @@ -0,0 +1,232 @@ + ALL logos, each with color:false (b/w) + * - customers selected -> the selected ones FIRST (selection order, + * color:true), followed by all remaining logos (color:false) + * + * `render()` = top-level plugin / page discovery. `renderForRecord()` = one + * specific tt_content row (reused by ContainerChildrenProcessor for nested + * plugins). Exception-safe. + */ +class CustomerlogosJsonRenderer +{ + private const TABLE = 'tx_vitec_domain_model_customer'; + private const IMAGE_WIDTHS = [200, 400]; + + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_customerlogos') { + return $this->renderForRecord($row); + } + + $pageId = 0; + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $pageInfo = $request->getAttribute('frontend.page.information'); + if ($pageInfo !== null) { + $pageId = (int)$pageInfo->getId(); + } + } + if ($pageId <= 0) { + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + } + if ($pageId <= 0) { + return ''; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $ces = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_customerlogos', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($ces)) { + return ''; + } + + return $this->renderForRecord($ces[0]); + } + + /** + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $layout = (string)($settings['layout'] ?? 'grid'); + $debugMode = (bool)($settings['debug'] ?? false); + $selectedUids = GeneralUtility::intExplode(',', (string)($settings['customers'] ?? ''), true); + $onlySelected = (bool)($settings['onlySelected'] ?? false); + + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE); + $rows = $qb + ->select('*') + ->from(self::TABLE) + ->where( + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $byUid = []; + foreach ($rows as $r) { + $byUid[(int)$r['uid']] = $r; + } + + // Selected customers first (selection order, in color), then the rest (b/w). + $logos = []; + foreach ($selectedUids as $uid) { + if (isset($byUid[$uid])) { + $logos[] = $this->serializeCustomer($byUid[$uid], true); + unset($byUid[$uid]); + } + } + if (!($onlySelected && $selectedUids !== [])) { + foreach ($byUid as $r) { + $logos[] = $this->serializeCustomer($r, false); + } + } + + $response = [ + 'layout' => $layout, + 'logos' => $logos, + ]; + + if ($debugMode) { + $response['debug'] = [ + 'count' => count($logos), + 'selected' => $selectedUids, + 'settings' => $settings, + ]; + } + + return (string)json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @param array $r + * @return array + */ + private function serializeCustomer(array $r, bool $color): array + { + return [ + 'id' => (int)$r['uid'], + 'name' => (string)($r['title'] ?? ''), + 'emphasized' => (bool)($r['emphasize_logo'] ?? false), + 'color' => $color, + 'logo' => $this->logo((int)$r['uid']), + ]; + } + + /** + * FAL logo with small srcset; SVGs are delivered unprocessed. + * + * @return array|null + */ + private function logo(int $customerUid): ?array + { + try { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_file_reference'); + $ref = $qb + ->select('uid', 'title', 'alternative', 'crop') + ->from('sys_file_reference') + ->where( + $qb->expr()->eq('tablenames', $qb->createNamedParameter(self::TABLE, ParameterType::STRING)), + $qb->expr()->eq('fieldname', $qb->createNamedParameter('logo', ParameterType::STRING)), + $qb->expr()->eq('uid_foreign', $qb->createNamedParameter($customerUid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting_foreign') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$ref) { + return null; + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $imageService = GeneralUtility::makeInstance(ImageService::class); + $fileReference = $resourceFactory->getFileReferenceObject((int)$ref['uid']); + + if (str_contains((string)$fileReference->getMimeType(), 'svg')) { + // SVG: no processing/srcset — deliver the file as-is. + return [ + 'uid' => (int)$ref['uid'], + 'url' => (string)$fileReference->getPublicUrl(), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'srcset' => [], + 'properties' => ['mimeType' => $fileReference->getMimeType()], + ]; + } + + $srcset = []; + foreach (self::IMAGE_WIDTHS as $width) { + $variant = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => $width, 'crop' => $ref['crop'] ?? null] + ); + $srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w']; + } + $default = $imageService->applyProcessingInstructions( + $fileReference, + ['width' => 400, 'crop' => $ref['crop'] ?? null] + ); + + return [ + 'uid' => (int)$ref['uid'], + 'url' => $imageService->getImageUri($default), + 'title' => (string)($ref['title'] ?? ''), + 'alternative' => (string)($ref['alternative'] ?? ''), + 'srcset' => $srcset, + 'properties' => [ + 'width' => $fileReference->getProperty('width'), + 'height' => $fileReference->getProperty('height'), + 'mimeType' => $fileReference->getProperty('mime_type'), + ], + ]; + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php index 7fbe5ab..0bd9bc1 100755 --- a/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php @@ -86,12 +86,14 @@ class EventlistJsonRenderer $daysInAdvance = (int)($settings['daysinadvance'] ?? 0); $limit = (int)($settings['limit'] ?? 0); $debugMode = (bool)($settings['debug'] ?? false); + $layout = (string)($settings['layout'] ?? 'list'); $upcoming = $this->fetchEvents(true, $limit, $daysInAdvance); $response = [ 'events' => array_map(fn($e) => $this->serializeEvent($e), $upcoming), 'settings' => [ + 'layout' => $layout, 'showpast' => $showPast, 'daysinadvance' => $daysInAdvance, 'limit' => $limit, diff --git a/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php b/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php new file mode 100755 index 0000000..49c118d --- /dev/null +++ b/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php @@ -0,0 +1,197 @@ +

"|null, "locations": [ … ] } + * + * Each location follows the frontend contract (id, slug, name, countryCode, + * coordinates, address, contact, links, marker, sorting, active). + * + * `render()` = top-level plugin / page discovery. `renderForRecord()` = one + * specific tt_content row (reused by ContainerChildrenProcessor for nested + * plugins). Exception-safe. + */ +class LocationsJsonRenderer +{ + private const TABLE = 'tx_vitec_domain_model_location'; + + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_locationlist') { + return $this->renderForRecord($row); + } + + $pageId = 0; + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $pageInfo = $request->getAttribute('frontend.page.information'); + if ($pageInfo !== null) { + $pageId = (int)$pageInfo->getId(); + } + } + if ($pageId <= 0) { + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + } + if ($pageId <= 0) { + return ''; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $ces = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_locationlist', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($ces)) { + return ''; + } + + return $this->renderForRecord($ces[0]); + } + + /** + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $variant = (string)($settings['variant'] ?? 'grid'); + $mapText = trim((string)($settings['maptext'] ?? '')); + $debugMode = (bool)($settings['debug'] ?? false); + + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE); + $rows = $qb + ->select('*') + ->from(self::TABLE) + ->where( + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $locations = array_map(fn(array $r): array => $this->serializeLocation($r), $rows); + + $response = [ + 'variant' => $variant, + 'mapText' => $variant === 'map' && $mapText !== '' ? $mapText : null, + 'locations' => $locations, + ]; + + if ($debugMode) { + $response['debug'] = ['count' => count($locations), 'settings' => $settings]; + } + + return (string)json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @param array $r + * @return array + */ + private function serializeLocation(array $r): array + { + $name = (string)($r['title'] ?? ''); + + return [ + 'id' => (int)$r['uid'], + 'slug' => (string)($r['slug'] ?? ''), + 'name' => $name, + 'countryCode' => (string)($r['country_code'] ?? ''), + 'coordinates' => [ + 'latitude' => (float)($r['latitude'] ?? 0), + 'longitude' => (float)($r['longitude'] ?? 0), + ], + 'address' => [ + 'company' => $this->nullIfEmpty($r['address_company'] ?? ''), + 'street' => $this->nullIfEmpty($r['street'] ?? ''), + 'additional' => $this->nullIfEmpty($r['address_additional'] ?? ''), + 'postalCode' => $this->nullIfEmpty($r['postal_code'] ?? ''), + 'city' => $this->nullIfEmpty($r['city'] ?? ''), + 'region' => $this->nullIfEmpty($r['region'] ?? ''), + 'country' => $this->nullIfEmpty($r['country'] ?? ''), + ], + 'contact' => [ + 'phone' => $this->nullIfEmpty($r['phone'] ?? ''), + 'fax' => $this->nullIfEmpty($r['fax'] ?? ''), + 'email' => $this->nullIfEmpty($r['email'] ?? ''), + ], + 'links' => [ + 'contact' => $this->resolveLink((string)($r['contact_link'] ?? '')), + 'legal' => $this->resolveLinkLines((string)($r['legal_links'] ?? '')), + ], + 'marker' => [ + 'label' => (string)($r['marker_label'] ?? '') !== '' ? (string)$r['marker_label'] : $name, + 'color' => (string)($r['marker_color'] ?? '#ff6633'), + 'size' => (float)($r['marker_size'] ?? 0.5), + ], + 'sorting' => (int)($r['sorting'] ?? 0), + 'active' => true, + ]; + } + + private function nullIfEmpty(mixed $value): ?string + { + $value = trim((string)$value); + return $value === '' ? null : $value; + } + + /** Resolve a typolink parameter (t3://…, page uid, URL) to a URL/path. */ + private function resolveLink(string $parameter): ?string + { + $parameter = trim($parameter); + if ($parameter === '') { + return null; + } + try { + $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $url = $cObj->typoLink_URL(['parameter' => $parameter]); + return $url !== '' ? $url : $parameter; + } catch (\Throwable $e) { + return $parameter; + } + } + + /** @return array */ + private function resolveLinkLines(string $lines): array + { + $out = []; + foreach (preg_split('/\r\n|\r|\n/', $lines) ?: [] as $line) { + $resolved = $this->resolveLink($line); + if ($resolved !== null) { + $out[] = $resolved; + } + } + return $out; + } +} diff --git a/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php b/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php new file mode 100755 index 0000000..8f84f5e --- /dev/null +++ b/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php @@ -0,0 +1,175 @@ + 'tx_vitec_domain_model_product', + 'story' => 'tx_vitec_domain_model_usecase', + 'market' => 'tx_vitec_domain_model_market', + 'solution' => 'tx_vitec_domain_model_solution', + ]; + + #[AsAllowedCallable] + public function render(string $content, array $conf): string + { + $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null; + if ($row && (string)($row['CType'] ?? '') === 'vitec_modelcard') { + return $this->renderForRecord($row); + } + + $pageId = 0; + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($request !== null) { + $pageInfo = $request->getAttribute('frontend.page.information'); + if ($pageInfo !== null) { + $pageId = (int)$pageInfo->getId(); + } + } + if ($pageId <= 0) { + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + } + if ($pageId <= 0) { + return ''; + } + + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $ces = $qb + ->select('*') + ->from('tt_content') + ->where( + $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), + $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_modelcard', ParameterType::STRING)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (empty($ces)) { + return ''; + } + + return $this->renderForRecord($ces[0]); + } + + /** + * @param array $contentElement + */ + public function renderForRecord(array $contentElement): string + { + try { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); + $settings = $flexFormData['settings'] ?? []; + + $modelType = (string)($settings['modelType'] ?? 'product'); + $layout = (string)($settings['layout'] ?? 'vertical'); + $debugMode = (bool)($settings['debug'] ?? false); + $recordUid = (int)($settings[$modelType === 'story' ? 'story' : $modelType] ?? 0); + + $item = null; + if ($recordUid > 0 && isset(self::MODEL_TABLES[$modelType])) { + $row = $this->fetchRecord(self::MODEL_TABLES[$modelType], $recordUid); + if ($row !== null) { + $item = $this->serializeItem($modelType, $row); + } + } + + $response = [ + 'modelType' => $modelType, + 'layout' => $layout, + 'item' => $item, + ]; + + if ($debugMode) { + $response['debug'] = ['recordUid' => $recordUid, 'settings' => $settings]; + } + + return (string)json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @return array|null + */ + private function fetchRecord(string $table, int $uid): ?array + { + $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $row = $qb + ->select('*') + ->from($table) + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + return $row ?: null; + } + + /** + * @param array $row + * @return array + */ + private function serializeItem(string $modelType, array $row): array + { + $serializer = GeneralUtility::makeInstance(UsecaseSerializer::class); + $uid = (int)$row['uid']; + + if ($modelType === 'story') { + // Success stories already have the canonical card serialisation. + return $serializer->serializeListItem($row); + } + + if ($modelType === 'product') { + return [ + 'uid' => $uid, + 'title' => (string)($row['title'] ?? ''), + 'slug' => (string)($row['slug'] ?? ''), + 'subtitle' => (string)($row['subtitle'] ?? ''), + 'teaser' => (string)($row['teaser'] ?? ''), + 'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product') + ?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product'), + ]; + } + + // market | solution — identical field set. + return [ + 'uid' => $uid, + 'title' => (string)($row['title'] ?? ''), + 'subtitle' => (string)($row['subtitle'] ?? ''), + 'teaser' => (string)($row['teaser'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + 'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType]), + ]; + } +} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index a413495..4b56db9 100755 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -11,6 +11,7 @@ use Evomedien\Vitec\Domain\Repository\ProductRepository; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -406,15 +407,61 @@ class ProductListJsonRenderer ->executeQuery() ->fetchAllAssociative(); - return array_map(static function ($cat) { + return array_map(function ($cat) { return [ 'uid' => (int)$cat['uid'], 'title' => $cat['title'] ?? '', 'description' => $cat['description'] ?? '', + 'parents' => $this->getCategoryParents((int)$cat['uid']), ]; }, $categories); } + /** @var array|null Lazy uid => [parent,title] map of all categories. */ + private ?array $categoryTreeMap = null; + + /** + * All ancestor categories of one category, ROOT FIRST (excluding itself). + * Backed by a once-per-request map of sys_category — cheap for any number + * of products. Cycle-safe. + * + * @return array + */ + private function getCategoryParents(int $categoryUid): array + { + if ($this->categoryTreeMap === null) { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + $rows = $qb + ->select('uid', 'parent', 'title') + ->from('sys_category') + ->where( + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + $this->categoryTreeMap = []; + foreach ($rows as $row) { + $this->categoryTreeMap[(int)$row['uid']] = [ + 'parent' => (int)$row['parent'], + 'title' => (string)$row['title'], + ]; + } + } + + $parents = []; + $seen = [$categoryUid => true]; + $current = $this->categoryTreeMap[$categoryUid]['parent'] ?? 0; + while ($current > 0 && isset($this->categoryTreeMap[$current]) && !isset($seen[$current])) { + $seen[$current] = true; + array_unshift($parents, ['uid' => $current, 'title' => $this->categoryTreeMap[$current]['title']]); + $current = $this->categoryTreeMap[$current]['parent']; + } + + return $parents; + } + private function getDownloadFileType(int $downloadUid): string { $qb = GeneralUtility::makeInstance(ConnectionPool::class) diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php index 2414b82..5f57403 100755 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -493,10 +493,56 @@ class ProductShowJsonRenderer 'uid' => (int)$cat['uid'], 'title' => $cat['title'], 'description' => $cat['description'] ?? '', + 'parents' => $this->getCategoryParents((int)$cat['uid']), ]; }, $categories); } + /** @var array|null Lazy uid => [parent,title] map of all categories. */ + private ?array $categoryTreeMap = null; + + /** + * All ancestor categories of one category, ROOT FIRST (excluding itself). + * Backed by a once-per-request map of sys_category — cheap for any number + * of products. Cycle-safe. + * + * @return array + */ + private function getCategoryParents(int $categoryUid): array + { + if ($this->categoryTreeMap === null) { + $qb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + $rows = $qb + ->select('uid', 'parent', 'title') + ->from('sys_category') + ->where( + $qb->expr()->eq('deleted', 0), + $qb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAllAssociative(); + $this->categoryTreeMap = []; + foreach ($rows as $row) { + $this->categoryTreeMap[(int)$row['uid']] = [ + 'parent' => (int)$row['parent'], + 'title' => (string)$row['title'], + ]; + } + } + + $parents = []; + $seen = [$categoryUid => true]; + $current = $this->categoryTreeMap[$categoryUid]['parent'] ?? 0; + while ($current > 0 && isset($this->categoryTreeMap[$current]) && !isset($seen[$current])) { + $seen[$current] = true; + array_unshift($parents, ['uid' => $current, 'title' => $this->categoryTreeMap[$current]['title']]); + $current = $this->categoryTreeMap[$current]['parent']; + } + + return $parents; + } + private function getDownloadFileType(int $downloadUid): string { $qb = GeneralUtility::makeInstance(ConnectionPool::class) diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php index 9d1d2b6..59df63a 100755 --- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php @@ -72,6 +72,22 @@ class UsecaseListJsonRenderer public function renderForRecord(array $contentElement): string { try { + // Detail context: when the request carries a story argument the + // list stays silent — the usecaseshow plugin on the same page + // renders the story. Keeps old SEO URLs on one page. + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + $routing = $request?->getAttribute('routing'); + $detailParam = null; + if ($routing instanceof \TYPO3\CMS\Core\Routing\PageArguments) { + $detailParam = $routing->getRouteArguments()['tx_vitec_usecaseshow']['usecase'] ?? null; + } + if ($detailParam === null || $detailParam === '') { + $detailParam = ($request?->getQueryParams() ?? [])['tx_vitec_usecaseshow']['usecase'] ?? null; + } + if ($detailParam !== null && $detailParam !== '') { + return ''; + } + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); $settings = $flexFormData['settings'] ?? []; @@ -92,8 +108,28 @@ class UsecaseListJsonRenderer ->fetchAllAssociative(); $serializer = GeneralUtility::makeInstance(UsecaseSerializer::class); + + // Detail links: the PUBLIC URL is the PARENT path of the + // FlexForm-selected "Single PID" page plus the story slug — + // /success-stories/, not /success-stories/story/. + // The SuccessStoryPathRewrite middleware maps it back to the + // detail subpage at request time. + $singlePid = (int)($settings['singlePid'] ?? 0); + $detailBase = ''; + if ($singlePid > 0) { + $detailPath = rtrim($this->resolvePageUrl($singlePid), '/'); + $parent = str_contains($detailPath, '/') ? substr($detailPath, 0, (int)strrpos($detailPath, '/')) : ''; + $detailBase = $parent !== '' ? $parent : $detailPath; + } + $usecases = array_map( - static fn (array $u): array => $serializer->serializeListItem($u), + static function (array $u) use ($serializer, $detailBase): array { + $item = $serializer->serializeListItem($u); + $item['detailUrl'] = ($detailBase !== '' && ($item['slug'] ?? '') !== '') + ? $detailBase . '/' . ltrim((string)$item['slug'], '/') + : null; + return $item; + }, $rows ); @@ -109,4 +145,15 @@ class UsecaseListJsonRenderer return ''; } } + + /** Resolve a page uid to its frontend path (route enhancer aware). */ + private function resolvePageUrl(int $pageUid): string + { + try { + $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class); + return (string)$cObj->typoLink_URL(['parameter' => (string)$pageUid]); + } catch (\Throwable $e) { + return ''; + } + } } diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php index 2d0925c..3143e20 100755 --- a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php @@ -77,16 +77,21 @@ class UsecaseShowJsonRenderer $layout = (string)($settings['layout'] ?? 'default'); $debugMode = (bool)($settings['debug'] ?? false); - $usecaseUid = (int)($settings['usecase'] ?? 0); - - // Fallback: uid/slug from the request (React detail route). - if (!$usecaseUid) { - $params = $GLOBALS['TYPO3_REQUEST']?->getQueryParams() ?? []; - $param = $params['tx_vitec_usecaseshow']['usecase'] ?? null; - if ($param !== null && $param !== '') { - $usecaseUid = is_numeric($param) ? (int)$param : $this->resolveSlug((string)$param); - } + // The story is resolved from the request ONLY (detail route from the + // list plugin's detailUrl): route-enhancer arguments live in the + // `routing` PageArguments, plain GET in query params. + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + $param = null; + $routing = $request?->getAttribute('routing'); + if ($routing instanceof \TYPO3\CMS\Core\Routing\PageArguments) { + $param = $routing->getRouteArguments()['tx_vitec_usecaseshow']['usecase'] ?? null; } + if ($param === null || $param === '') { + $param = ($request?->getQueryParams() ?? [])['tx_vitec_usecaseshow']['usecase'] ?? null; + } + $usecaseUid = ($param !== null && $param !== '') + ? (is_numeric($param) ? (int)$param : $this->resolveSlug((string)$param)) + : 0; if (!$usecaseUid) { return $debugMode @@ -114,9 +119,11 @@ class UsecaseShowJsonRenderer $serializer = GeneralUtility::makeInstance(UsecaseSerializer::class); + $backPid = (int)($settings['backPid'] ?? 0); $response = [ 'usecase' => $serializer->serializeDetail($usecase), 'layout' => $layout, + 'backUrl' => $backPid > 0 ? ($this->resolvePageUrl($backPid) ?: null) : null, ]; if ($debugMode) { @@ -145,4 +152,15 @@ class UsecaseShowJsonRenderer return (int)($row['uid'] ?? 0); } + + /** Resolve a page uid to its frontend path (route enhancer aware). */ + private function resolvePageUrl(int $pageUid): string + { + try { + $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class); + return (string)$cObj->typoLink_URL(['parameter' => (string)$pageUid]); + } catch (\Throwable $e) { + return ''; + } + } } diff --git a/packages/vitec/Configuration/FlexForms/Customerlogos.xml b/packages/vitec/Configuration/FlexForms/Customerlogos.xml new file mode 100755 index 0000000..7ad25eb --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Customerlogos.xml @@ -0,0 +1,73 @@ + + + + 1 + + + + + Customer Logos Settings + array + + + + + select + selectSingle + + + + list + + + + grid + + + + carousel + + + + marquee + + + grid + + + + + + Empty = all logos black & white. Selected customers are shown in color, in front of the remaining (b/w) logos. + + select + selectMultipleSideBySide + tx_vitec_domain_model_customer + AND tx_vitec_domain_model_customer.hidden = 0 AND tx_vitec_domain_model_customer.deleted = 0 ORDER BY tx_vitec_domain_model_customer.title + 8 + 0 + 999 + + + + + + Show only the highlighted customers — the remaining (b/w) logos are omitted entirely. + + check + 0 + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Eventlist.xml b/packages/vitec/Configuration/FlexForms/Eventlist.xml index c6e1b36..541385a 100755 --- a/packages/vitec/Configuration/FlexForms/Eventlist.xml +++ b/packages/vitec/Configuration/FlexForms/Eventlist.xml @@ -9,6 +9,29 @@ Event List Settings array + + + + select + selectSingle + + + + list + + + + grid + + + + teaserbar + + + list + + + Only show upcoming events starting within the next N days, e.g. 60. diff --git a/packages/vitec/Configuration/FlexForms/Locationlist.xml b/packages/vitec/Configuration/FlexForms/Locationlist.xml new file mode 100755 index 0000000..42b047b --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Locationlist.xml @@ -0,0 +1,57 @@ + + + + 1 + + + + + Locations Settings + array + + + + + select + selectSingle + + + + grid + + + + list + + + + map + + + grid + + + + + + Rich text shown alongside the map (Map variant only). + FIELD:settings.variant:=:map + + text + 8 + 1 + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Modelcard.xml b/packages/vitec/Configuration/FlexForms/Modelcard.xml new file mode 100755 index 0000000..a39cba9 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Modelcard.xml @@ -0,0 +1,146 @@ + + + + 1 + + + + + Card Settings + array + + + + reload + + select + selectSingle + + + + product + + + + story + + + + market + + + + solution + + + product + + + + + + FIELD:settings.modelType:=:product + + select + selectSingle + + + + 0 + + + tx_vitec_domain_model_product + AND tx_vitec_domain_model_product.hidden = 0 AND tx_vitec_domain_model_product.deleted = 0 ORDER BY tx_vitec_domain_model_product.title + + + + + + FIELD:settings.modelType:=:story + + select + selectSingle + + + + 0 + + + tx_vitec_domain_model_usecase + AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title + + + + + + FIELD:settings.modelType:=:market + + select + selectSingle + + + + 0 + + + tx_vitec_domain_model_market + AND tx_vitec_domain_model_market.hidden = 0 AND tx_vitec_domain_model_market.deleted = 0 ORDER BY tx_vitec_domain_model_market.title + + + + + + FIELD:settings.modelType:=:solution + + select + selectSingle + + + + 0 + + + tx_vitec_domain_model_solution + AND tx_vitec_domain_model_solution.hidden = 0 AND tx_vitec_domain_model_solution.deleted = 0 ORDER BY tx_vitec_domain_model_solution.title + + + + + + + select + selectSingle + + + + vertical + + + + horizontal + + + + overlay + + + + compact + + + vertical + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Usecase.xml b/packages/vitec/Configuration/FlexForms/Usecase.xml index f026803..7da534c 100644 --- a/packages/vitec/Configuration/FlexForms/Usecase.xml +++ b/packages/vitec/Configuration/FlexForms/Usecase.xml @@ -8,6 +8,17 @@ array + + + Page that holds the Success Story list plugin. Used to build the back link in the JSON output. + + group + pages + 1 + 1 + + + @@ -19,20 +30,6 @@ - - - - select - selectSingle - tx_vitec_domain_model_usecase - AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title - 1 - 0 - 1 - -