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 <noreply@anthropic.com>
This commit is contained in:
708
packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php
Executable file
708
packages/vitec/Classes/Command/ImportSuccessStoriesCommand.php
Executable file
@@ -0,0 +1,708 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* One-time (idempotent) migration of the old vitec.com Success Stories into
|
||||
* the new tx_vitec_domain_model_usecase model.
|
||||
*
|
||||
* Source: migrations JSON produced on the OLD site by
|
||||
* migrations/export_success_stories.php, plus the staged original files under
|
||||
* fileadmin/success_stories_import/ (same relative identifiers as on the old
|
||||
* site).
|
||||
*
|
||||
* Mapping (see analysis in the session / architecture spec):
|
||||
* pages(title,slug,SEO) -> 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("<error>Export file not found: $file</error>");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$this->export = json_decode((string)file_get_contents($file), true) ?? [];
|
||||
if (!isset($this->export['pages'])) {
|
||||
$output->writeln('<error>Export JSON malformed (no pages key)</error>');
|
||||
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('<error>No storage pid — pass --pid or create one usecase record manually first.</error>');
|
||||
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(
|
||||
'<info>%d stories in export · storage pid %d · %s</info>',
|
||||
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("<error> $slugTail: {$e->getMessage()}</error>");
|
||||
}
|
||||
}
|
||||
|
||||
$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(" <info>created market: {$fields['title']} ($uid)</info>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @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(
|
||||
' <comment>%s</comment> 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<int,array<string,mixed>> 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[^>]*>(?:(?!<\/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'] ?? ''), '<blockquote') === false;
|
||||
$cols = array_keys($byCol);
|
||||
$allSimple = count($cols) === 2 && count($children) <= 4
|
||||
&& array_reduce($children, static fn($ok, $c) => $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') !== false) {
|
||||
$quote = $body;
|
||||
$name = '';
|
||||
$position = '';
|
||||
if (preg_match('/<blockquote[^>]*>(.*)<\/blockquote>/is', $body, $m)) {
|
||||
$inner = $m[1];
|
||||
// attribution paragraph: <p><strong>— Name</strong><br/>Position</p>
|
||||
if (preg_match('/<p>\s*<strong>\s*(?:—|—|-)?\s*(.*?)<\/strong>\s*(?:<br\s*\/?>)?\s*(.*?)<\/p>\s*$/is', $inner, $a)) {
|
||||
$name = trim(strip_tags($a[1]));
|
||||
$position = trim(strip_tags($a[2]));
|
||||
$inner = (string)preg_replace('/<p>\s*<strong>\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;
|
||||
}
|
||||
}
|
||||
21
packages/vitec/Classes/Controller/CustomerController.php
Executable file
21
packages/vitec/Classes/Controller/CustomerController.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* CustomerController — Extbase registration target for the Customerlogos
|
||||
* plugin. In headless mode the JSON output is produced by
|
||||
* CustomerlogosJsonRenderer, not by this controller.
|
||||
*/
|
||||
class CustomerController extends ActionController
|
||||
{
|
||||
public function listAction(): ResponseInterface
|
||||
{
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
21
packages/vitec/Classes/Controller/LocationController.php
Executable file
21
packages/vitec/Classes/Controller/LocationController.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* LocationController — Extbase registration target for the Locationlist
|
||||
* plugin. In headless mode the JSON output is produced by
|
||||
* LocationsJsonRenderer, not by this controller.
|
||||
*/
|
||||
class LocationController extends ActionController
|
||||
{
|
||||
public function listAction(): ResponseInterface
|
||||
{
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
21
packages/vitec/Classes/Controller/ModelcardController.php
Executable file
21
packages/vitec/Classes/Controller/ModelcardController.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* ModelcardController — Extbase registration target for the VITEC Card
|
||||
* plugin. In headless mode the JSON output is produced by
|
||||
* ModelcardJsonRenderer, not by this controller.
|
||||
*/
|
||||
class ModelcardController extends ActionController
|
||||
{
|
||||
public function listAction(): ResponseInterface
|
||||
{
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
79
packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php
Executable file
79
packages/vitec/Classes/Middleware/SuccessStoryPathRewrite.php
Executable file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Pretty detail URLs for Success Stories.
|
||||
*
|
||||
* Public URL: /success-stories/<slug> (old-site SEO URLs)
|
||||
* Real page: /success-stories/story/<slug> (subpage holding ONLY the
|
||||
* usecaseshow plugin)
|
||||
*
|
||||
* TYPO3's page router always resolves /success-stories/<slug> 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 <slug> 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;
|
||||
}
|
||||
}
|
||||
127
packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php
Executable file
127
packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Preview;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer;
|
||||
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
|
||||
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\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Page-module preview for the VITEC Card plugin (vitec_modelcard):
|
||||
* type badge, selected record title, thumbnail and layout badge.
|
||||
* Registered via TCA types.vitec_modelcard.previewRenderer.
|
||||
*/
|
||||
class ModelcardPreviewRenderer extends StandardContentPreviewRenderer
|
||||
{
|
||||
private const MODEL_TABLES = [
|
||||
'product' => '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 '';
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<int,array<string,mixed>>
|
||||
*/
|
||||
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<string,mixed>|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'),
|
||||
];
|
||||
|
||||
232
packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php
Executable file
232
packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php
Executable file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
|
||||
/**
|
||||
* UserFunc: render the VITEC customer logos as JSON (headless).
|
||||
*
|
||||
* Output under content.customerlogos:
|
||||
* { "layout": "list|grid|carousel|marquee", "logos": [ … ] }
|
||||
*
|
||||
* Selection logic:
|
||||
* - no customers selected -> 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<string,mixed> $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<string,mixed> $r
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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<string,mixed>|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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
197
packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
Executable file
197
packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
Executable file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* UserFunc: render the VITEC locations as JSON (headless).
|
||||
*
|
||||
* Output under content.locations:
|
||||
* { "variant": "grid|list|map", "mapText": "<p>…</p>"|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<string,mixed> $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<string,mixed> $r
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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<int,string> */
|
||||
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;
|
||||
}
|
||||
}
|
||||
175
packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php
Executable file
175
packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php
Executable file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* UserFunc: render the VITEC Card plugin as JSON (headless).
|
||||
*
|
||||
* ONE plugin for four card types — the FlexForm picks the model type
|
||||
* (product | story | market | solution) plus one record; ALL card data
|
||||
* comes from the model. Output under content.card:
|
||||
*
|
||||
* { "modelType": "product", "layout": "vertical", "item": { … } }
|
||||
*
|
||||
* Image resolution is delegated to UsecaseSerializer::image() (generic,
|
||||
* srcset + SVG-safe). `render()` = top-level plugin / page discovery,
|
||||
* `renderForRecord()` = one tt_content row (container-nestable).
|
||||
* Exception-safe.
|
||||
*/
|
||||
class ModelcardJsonRenderer
|
||||
{
|
||||
private const MODEL_TABLES = [
|
||||
'product' => '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<string,mixed> $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<string,mixed>|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<string,mixed> $row
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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<int,array{parent:int,title:string}>|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<int,array{uid:int,title:string}>
|
||||
*/
|
||||
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)
|
||||
|
||||
@@ -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<int,array{parent:int,title:string}>|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<int,array{uid:int,title:string}>
|
||||
*/
|
||||
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)
|
||||
|
||||
@@ -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/<slug>, not /success-stories/story/<slug>.
|
||||
// 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
packages/vitec/Configuration/FlexForms/Customerlogos.xml
Executable file
73
packages/vitec/Configuration/FlexForms/Customerlogos.xml
Executable file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Customer Logos Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.layout>
|
||||
<label>Layout</label>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>List</label>
|
||||
<value>list</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Grid</label>
|
||||
<value>grid</value>
|
||||
</numIndex>
|
||||
<numIndex index="2">
|
||||
<label>Carousel</label>
|
||||
<value>carousel</value>
|
||||
</numIndex>
|
||||
<numIndex index="3">
|
||||
<label>Marquee (scrolling ticker)</label>
|
||||
<value>marquee</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>grid</default>
|
||||
</config>
|
||||
</settings.layout>
|
||||
|
||||
<settings.customers>
|
||||
<label>Highlighted Customers</label>
|
||||
<description>Empty = all logos black & white. Selected customers are shown in color, in front of the remaining (b/w) logos.</description>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectMultipleSideBySide</renderType>
|
||||
<foreign_table>tx_vitec_domain_model_customer</foreign_table>
|
||||
<foreign_table_where>AND tx_vitec_domain_model_customer.hidden = 0 AND tx_vitec_domain_model_customer.deleted = 0 ORDER BY tx_vitec_domain_model_customer.title</foreign_table_where>
|
||||
<size>8</size>
|
||||
<minitems>0</minitems>
|
||||
<maxitems>999</maxitems>
|
||||
</config>
|
||||
</settings.customers>
|
||||
|
||||
<settings.onlySelected>
|
||||
<label>Only show selected</label>
|
||||
<description>Show only the highlighted customers — the remaining (b/w) logos are omitted entirely.</description>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.onlySelected>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
@@ -9,6 +9,29 @@
|
||||
<sheetTitle>Event List Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.layout>
|
||||
<label>Layout</label>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>List View</label>
|
||||
<value>list</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Grid View</label>
|
||||
<value>grid</value>
|
||||
</numIndex>
|
||||
<numIndex index="2">
|
||||
<label>Teaser Bar</label>
|
||||
<value>teaserbar</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>list</default>
|
||||
</config>
|
||||
</settings.layout>
|
||||
|
||||
<settings.daysinadvance>
|
||||
<label>Days in advance (0 = no limit)</label>
|
||||
<description>Only show upcoming events starting within the next N days, e.g. 60.</description>
|
||||
|
||||
57
packages/vitec/Configuration/FlexForms/Locationlist.xml
Executable file
57
packages/vitec/Configuration/FlexForms/Locationlist.xml
Executable file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Locations Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.variant>
|
||||
<label>Display Variant</label>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>Grid</label>
|
||||
<value>grid</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>List</label>
|
||||
<value>list</value>
|
||||
</numIndex>
|
||||
<numIndex index="2">
|
||||
<label>Map</label>
|
||||
<value>map</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>grid</default>
|
||||
</config>
|
||||
</settings.variant>
|
||||
|
||||
<settings.maptext>
|
||||
<label>Map Text</label>
|
||||
<description>Rich text shown alongside the map (Map variant only).</description>
|
||||
<displayCond>FIELD:settings.variant:=:map</displayCond>
|
||||
<config>
|
||||
<type>text</type>
|
||||
<rows>8</rows>
|
||||
<enableRichtext>1</enableRichtext>
|
||||
</config>
|
||||
</settings.maptext>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
146
packages/vitec/Configuration/FlexForms/Modelcard.xml
Executable file
146
packages/vitec/Configuration/FlexForms/Modelcard.xml
Executable file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Card Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.modelType>
|
||||
<label>Card Type</label>
|
||||
<onChange>reload</onChange>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>Product</label>
|
||||
<value>product</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Success Story</label>
|
||||
<value>story</value>
|
||||
</numIndex>
|
||||
<numIndex index="2">
|
||||
<label>Market</label>
|
||||
<value>market</value>
|
||||
</numIndex>
|
||||
<numIndex index="3">
|
||||
<label>Solution</label>
|
||||
<value>solution</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>product</default>
|
||||
</config>
|
||||
</settings.modelType>
|
||||
|
||||
<settings.product>
|
||||
<label>Product</label>
|
||||
<displayCond>FIELD:settings.modelType:=:product</displayCond>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>— please choose —</label>
|
||||
<value>0</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<foreign_table>tx_vitec_domain_model_product</foreign_table>
|
||||
<foreign_table_where>AND tx_vitec_domain_model_product.hidden = 0 AND tx_vitec_domain_model_product.deleted = 0 ORDER BY tx_vitec_domain_model_product.title</foreign_table_where>
|
||||
</config>
|
||||
</settings.product>
|
||||
|
||||
<settings.story>
|
||||
<label>Success Story</label>
|
||||
<displayCond>FIELD:settings.modelType:=:story</displayCond>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>— please choose —</label>
|
||||
<value>0</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<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>
|
||||
</config>
|
||||
</settings.story>
|
||||
|
||||
<settings.market>
|
||||
<label>Market</label>
|
||||
<displayCond>FIELD:settings.modelType:=:market</displayCond>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>— please choose —</label>
|
||||
<value>0</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<foreign_table>tx_vitec_domain_model_market</foreign_table>
|
||||
<foreign_table_where>AND tx_vitec_domain_model_market.hidden = 0 AND tx_vitec_domain_model_market.deleted = 0 ORDER BY tx_vitec_domain_model_market.title</foreign_table_where>
|
||||
</config>
|
||||
</settings.market>
|
||||
|
||||
<settings.solution>
|
||||
<label>Solution</label>
|
||||
<displayCond>FIELD:settings.modelType:=:solution</displayCond>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>— please choose —</label>
|
||||
<value>0</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<foreign_table>tx_vitec_domain_model_solution</foreign_table>
|
||||
<foreign_table_where>AND tx_vitec_domain_model_solution.hidden = 0 AND tx_vitec_domain_model_solution.deleted = 0 ORDER BY tx_vitec_domain_model_solution.title</foreign_table_where>
|
||||
</config>
|
||||
</settings.solution>
|
||||
|
||||
<settings.layout>
|
||||
<label>Layout</label>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>Vertical (image top)</label>
|
||||
<value>vertical</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Horizontal (image left)</label>
|
||||
<value>horizontal</value>
|
||||
</numIndex>
|
||||
<numIndex index="2">
|
||||
<label>Image Overlay</label>
|
||||
<value>overlay</value>
|
||||
</numIndex>
|
||||
<numIndex index="3">
|
||||
<label>Compact</label>
|
||||
<value>compact</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>vertical</default>
|
||||
</config>
|
||||
</settings.layout>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
@@ -8,6 +8,17 @@
|
||||
</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.backPid>
|
||||
<label>Back to List View (page)</label>
|
||||
<description>Page that holds the Success Story list plugin. Used to build the back link in the JSON output.</description>
|
||||
<config>
|
||||
<type>group</type>
|
||||
<allowed>pages</allowed>
|
||||
<size>1</size>
|
||||
<maxitems>1</maxitems>
|
||||
</config>
|
||||
</settings.backPid>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output.</label>
|
||||
<config>
|
||||
@@ -19,20 +30,6 @@
|
||||
</items>
|
||||
</config>
|
||||
</settings.debug>
|
||||
<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>
|
||||
<settings.layout>
|
||||
<label>
|
||||
Layout
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
<sheetTitle>Usecase List</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.singlePid>
|
||||
<label>Single PID (detail page)</label>
|
||||
<description>Page that holds the "Show single VITEC Success Story" plugin. Used to build the detail links in the JSON output.</description>
|
||||
<config>
|
||||
<type>group</type>
|
||||
<allowed>pages</allowed>
|
||||
<size>1</size>
|
||||
<maxitems>1</maxitems>
|
||||
</config>
|
||||
</settings.singlePid>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output.</label>
|
||||
<config>
|
||||
|
||||
@@ -76,4 +76,16 @@ return [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg',
|
||||
],
|
||||
'vitec-plugin-locationlist' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg',
|
||||
],
|
||||
'vitec-plugin-customerlogos' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg',
|
||||
],
|
||||
'vitec-plugin-modelcard' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg',
|
||||
],
|
||||
];
|
||||
|
||||
20
packages/vitec/Configuration/RequestMiddlewares.php
Executable file
20
packages/vitec/Configuration/RequestMiddlewares.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Frontend middleware registration for EXT:vitec.
|
||||
*/
|
||||
return [
|
||||
'frontend' => [
|
||||
'vitec/success-story-path-rewrite' => [
|
||||
'target' => \Evomedien\Vitec\Middleware\SuccessStoryPathRewrite::class,
|
||||
'after' => [
|
||||
'typo3/cms-core/normalized-params-attribute',
|
||||
],
|
||||
'before' => [
|
||||
'typo3/cms-frontend/site',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -160,6 +160,42 @@ tt_content {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vitec_modelcard < lib.contentElementWithHeader
|
||||
vitec_modelcard {
|
||||
fields {
|
||||
content {
|
||||
fields {
|
||||
card = USER
|
||||
card.userFunc = Evomedien\Vitec\UserFunc\ModelcardJsonRenderer->render
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vitec_customerlogos < lib.contentElementWithHeader
|
||||
vitec_customerlogos {
|
||||
fields {
|
||||
content {
|
||||
fields {
|
||||
customerlogos = USER
|
||||
customerlogos.userFunc = Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer->render
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vitec_locationlist < lib.contentElementWithHeader
|
||||
vitec_locationlist {
|
||||
fields {
|
||||
content {
|
||||
fields {
|
||||
locations = USER
|
||||
locations.userFunc = Evomedien\Vitec\UserFunc\LocationsJsonRenderer->render
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Include container (layout) rendering definitions
|
||||
|
||||
@@ -35,6 +35,41 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||
$registerPluginWithFlexForm('Downloadcardcollection', 'Download Card Collection', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml', 'vitec-plugin-downloadcardcollection');
|
||||
$registerPluginWithFlexForm('Datasheets', 'Datasheets', 'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml');
|
||||
$registerPluginWithFlexForm('Eventlist', 'Event List', 'FILE:EXT:vitec/Configuration/FlexForms/Eventlist.xml', 'vitec-plugin-eventlist');
|
||||
// Locations: lives in the "VITEC" wizard group, titled "VITEC Locations".
|
||||
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec']
|
||||
= 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header';
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Modelcard',
|
||||
'VITEC Card',
|
||||
'vitec-plugin-modelcard',
|
||||
'vitec',
|
||||
'One card for a Product, Success Story, Market or Solution — all data from the model.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Modelcard.xml'
|
||||
);
|
||||
$GLOBALS['TCA']['tt_content']['types']['vitec_modelcard']['previewRenderer']
|
||||
= \Evomedien\Vitec\Preview\ModelcardPreviewRenderer::class;
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Customerlogos',
|
||||
'VITEC Customer Logos',
|
||||
'vitec-plugin-customerlogos',
|
||||
'vitec',
|
||||
'Customer logos in list, grid, carousel or marquee layout.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Customerlogos.xml'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Locationlist',
|
||||
'VITEC Locations',
|
||||
'vitec-plugin-locationlist',
|
||||
'vitec',
|
||||
'Locations as grid, list or interactive map.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Locationlist.xml'
|
||||
);
|
||||
|
||||
|
||||
// Change frame_class to allow multiple selections.
|
||||
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
|
||||
|
||||
63
packages/vitec/Configuration/TCA/tx_vitec_domain_model_customer.php
Executable file
63
packages/vitec/Configuration/TCA/tx_vitec_domain_model_customer.php
Executable file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
return [
|
||||
'ctrl' => [
|
||||
'title' => 'VITEC Customer',
|
||||
'label' => 'title',
|
||||
'tstamp' => 'tstamp',
|
||||
'crdate' => 'crdate',
|
||||
'sortby' => 'sorting',
|
||||
'delete' => 'deleted',
|
||||
'enablecolumns' => [
|
||||
'disabled' => 'hidden',
|
||||
'starttime' => 'starttime',
|
||||
'endtime' => 'endtime',
|
||||
],
|
||||
'searchFields' => 'title',
|
||||
'iconfile' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg',
|
||||
'security' => [
|
||||
'ignorePageTypeRestriction' => true,
|
||||
],
|
||||
],
|
||||
'types' => [
|
||||
'1' => [
|
||||
'showitem' =>
|
||||
'title, logo, emphasize_logo,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
|
||||
hidden, starttime, endtime',
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'hidden' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
'renderType' => 'checkboxToggle',
|
||||
'items' => [['value' => '', 'label' => '', 'invertStateDisplay' => true]],
|
||||
],
|
||||
],
|
||||
'starttime' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
|
||||
'config' => ['type' => 'datetime', 'default' => 0],
|
||||
],
|
||||
'endtime' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
|
||||
'config' => ['type' => 'datetime', 'default' => 0, 'range' => ['upper' => mktime(0, 0, 0, 1, 1, 2038)]],
|
||||
],
|
||||
'title' => [
|
||||
'label' => 'Customer Name',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'required' => true, 'default' => ''],
|
||||
],
|
||||
'logo' => [
|
||||
'label' => 'Logo',
|
||||
'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'],
|
||||
],
|
||||
'emphasize_logo' => [
|
||||
'label' => 'Emphasize Logo',
|
||||
'description' => 'Highlight this logo (e.g. larger) in the frontend.',
|
||||
'config' => ['type' => 'check', 'renderType' => 'checkboxToggle', 'items' => [['value' => '', 'label' => '']]],
|
||||
],
|
||||
],
|
||||
];
|
||||
168
packages/vitec/Configuration/TCA/tx_vitec_domain_model_location.php
Executable file
168
packages/vitec/Configuration/TCA/tx_vitec_domain_model_location.php
Executable file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
return [
|
||||
'ctrl' => [
|
||||
'title' => 'VITEC Location',
|
||||
'label' => 'title',
|
||||
'tstamp' => 'tstamp',
|
||||
'crdate' => 'crdate',
|
||||
'sortby' => 'sorting',
|
||||
'delete' => 'deleted',
|
||||
'enablecolumns' => [
|
||||
'disabled' => 'hidden',
|
||||
'starttime' => 'starttime',
|
||||
'endtime' => 'endtime',
|
||||
],
|
||||
'searchFields' => 'title,slug,city,country,country_code',
|
||||
'iconfile' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg',
|
||||
'security' => [
|
||||
'ignorePageTypeRestriction' => true,
|
||||
],
|
||||
],
|
||||
'types' => [
|
||||
'1' => [
|
||||
'showitem' =>
|
||||
'--div--;General,
|
||||
title, slug, country_code,
|
||||
--palette--;Coordinates;coords,
|
||||
--div--;Address,
|
||||
address_company, street, address_additional,
|
||||
--palette--;;cityline,
|
||||
region, country,
|
||||
--div--;Contact & Links,
|
||||
phone, fax, email, contact_link, legal_links,
|
||||
--div--;Marker,
|
||||
marker_label, marker_color, marker_size,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
|
||||
hidden, starttime, endtime',
|
||||
],
|
||||
],
|
||||
'palettes' => [
|
||||
'coords' => ['showitem' => 'latitude, longitude'],
|
||||
'cityline' => ['showitem' => 'postal_code, city'],
|
||||
],
|
||||
'columns' => [
|
||||
'hidden' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
'renderType' => 'checkboxToggle',
|
||||
'items' => [['value' => '', 'label' => '', 'invertStateDisplay' => true]],
|
||||
],
|
||||
],
|
||||
'starttime' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
|
||||
'config' => ['type' => 'datetime', 'default' => 0],
|
||||
],
|
||||
'endtime' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
|
||||
'config' => ['type' => 'datetime', 'default' => 0, 'range' => ['upper' => mktime(0, 0, 0, 1, 1, 2038)]],
|
||||
],
|
||||
|
||||
// ----------------------------------------------------------- General
|
||||
'title' => [
|
||||
'label' => 'Name',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'required' => true, 'default' => ''],
|
||||
],
|
||||
'slug' => [
|
||||
'label' => 'Slug',
|
||||
'config' => [
|
||||
'type' => 'slug',
|
||||
'size' => 50,
|
||||
'generatorOptions' => ['fields' => ['title'], 'fieldSeparator' => '-', 'replacements' => ['/' => '', ',' => '']],
|
||||
'fallbackCharacter' => '-',
|
||||
'eval' => 'uniqueInPid',
|
||||
],
|
||||
],
|
||||
'country_code' => [
|
||||
'label' => 'Country Code (ISO 3166-1 alpha-2)',
|
||||
'description' => 'e.g. US, DE, GB',
|
||||
'config' => ['type' => 'input', 'size' => 6, 'max' => 5, 'eval' => 'trim,upper', 'default' => ''],
|
||||
],
|
||||
'latitude' => [
|
||||
'label' => 'Latitude',
|
||||
'config' => ['type' => 'number', 'format' => 'decimal', 'default' => 0, 'range' => ['lower' => -90, 'upper' => 90]],
|
||||
],
|
||||
'longitude' => [
|
||||
'label' => 'Longitude',
|
||||
'config' => ['type' => 'number', 'format' => 'decimal', 'default' => 0, 'range' => ['lower' => -180, 'upper' => 180]],
|
||||
],
|
||||
|
||||
// ----------------------------------------------------------- Address
|
||||
'address_company' => [
|
||||
'label' => 'Company (optional)',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'street' => [
|
||||
'label' => 'Street',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'address_additional' => [
|
||||
'label' => 'Additional (optional)',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'postal_code' => [
|
||||
'label' => 'Postal Code',
|
||||
'config' => ['type' => 'input', 'size' => 10, 'max' => 20, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'city' => [
|
||||
'label' => 'City',
|
||||
'config' => ['type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'region' => [
|
||||
'label' => 'Region / State (optional)',
|
||||
'description' => 'e.g. GA',
|
||||
'config' => ['type' => 'input', 'size' => 20, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'country' => [
|
||||
'label' => 'Country (display name)',
|
||||
'config' => ['type' => 'input', 'size' => 30, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
|
||||
// --------------------------------------------------- Contact & Links
|
||||
'phone' => [
|
||||
'label' => 'Phone',
|
||||
'config' => ['type' => 'input', 'size' => 30, 'max' => 100, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'fax' => [
|
||||
'label' => 'Fax',
|
||||
'config' => ['type' => 'input', 'size' => 30, 'max' => 100, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'email' => [
|
||||
'label' => 'E-Mail',
|
||||
'config' => ['type' => 'email', 'size' => 40, 'default' => ''],
|
||||
],
|
||||
'contact_link' => [
|
||||
'label' => 'Contact Link',
|
||||
'config' => ['type' => 'link', 'allowedTypes' => ['page', 'url', 'email']],
|
||||
],
|
||||
'legal_links' => [
|
||||
'label' => 'Legal Links',
|
||||
'description' => 'One link per line (page link or URL).',
|
||||
'config' => ['type' => 'text', 'cols' => 40, 'rows' => 3, 'eval' => 'trim'],
|
||||
],
|
||||
|
||||
// ------------------------------------------------------------ Marker
|
||||
'marker_label' => [
|
||||
'label' => 'Marker Label (optional)',
|
||||
'description' => 'Falls back to the location name.',
|
||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
||||
],
|
||||
'marker_color' => [
|
||||
'label' => 'Marker Color',
|
||||
'config' => ['type' => 'color', 'default' => '#ff6633'],
|
||||
],
|
||||
'marker_size' => [
|
||||
'label' => 'Marker Size (scale, e.g. 0.5)',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'format' => 'decimal',
|
||||
'default' => 0.5,
|
||||
'range' => ['lower' => 0, 'upper' => 5],
|
||||
'slider' => ['step' => 0.05, 'width' => 200],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -34,7 +34,7 @@ return [
|
||||
--div--;Detail · Content,
|
||||
content_elements,
|
||||
--div--;Detail · Related,
|
||||
market, solutions, products, categories,
|
||||
markets, solutions, products, categories,
|
||||
--div--;SEO,
|
||||
seo_title, seo_description, no_index, no_follow, canonical_link,
|
||||
--palette--;Open Graph;ogPalette,
|
||||
@@ -182,15 +182,16 @@ return [
|
||||
],
|
||||
|
||||
// --------------------------------------------------- Detail · Related
|
||||
'market' => [
|
||||
'label' => 'Market',
|
||||
'markets' => [
|
||||
'label' => 'Markets',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'foreign_table' => 'tx_vitec_domain_model_market',
|
||||
'items' => [['label' => '', 'value' => 0]],
|
||||
'default' => 0,
|
||||
'maxitems' => 1,
|
||||
'MM' => 'tx_vitec_usecase_market_mm',
|
||||
'size' => 6,
|
||||
'autoSizeMax' => 20,
|
||||
'maxitems' => 9999,
|
||||
],
|
||||
],
|
||||
'solutions' => [
|
||||
|
||||
@@ -4,6 +4,11 @@ prefixFields: true
|
||||
prefixType: vendor
|
||||
|
||||
fields:
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
- identifier: tab_content
|
||||
type: Tab
|
||||
label: Content
|
||||
|
||||
- identifier: eyebrow
|
||||
type: Text
|
||||
max: 50
|
||||
@@ -29,10 +34,6 @@ fields:
|
||||
- identifier: subheader
|
||||
useExistingField: true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- identifier: bodytext
|
||||
useExistingField: true
|
||||
enableRichtext: true
|
||||
@@ -53,7 +54,33 @@ fields:
|
||||
- identifier: Vitec/ButtonStyle
|
||||
type: Basic
|
||||
|
||||
# --- Image ---
|
||||
- identifier: hero_layout_variant
|
||||
type: Select
|
||||
renderType: selectSingle
|
||||
default: fullscreen
|
||||
items:
|
||||
- label: Fullscreen
|
||||
value: fullscreen
|
||||
- label: Medium
|
||||
value: md
|
||||
- label: Small
|
||||
value: sm
|
||||
- label: Extra Small
|
||||
value: xs
|
||||
|
||||
- identifier: show_logo_wall
|
||||
type: Checkbox
|
||||
default: 0
|
||||
|
||||
- identifier: debug
|
||||
type: Checkbox
|
||||
default: 0
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
- identifier: tab_images
|
||||
type: Tab
|
||||
label: Images
|
||||
|
||||
- identifier: hero_bgimage
|
||||
type: File
|
||||
minitems: 0
|
||||
@@ -82,7 +109,11 @@ fields:
|
||||
- label: Fullwidth
|
||||
value: fullwidth
|
||||
|
||||
# --- Video ---
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
- identifier: tab_video
|
||||
type: Tab
|
||||
label: Video
|
||||
|
||||
- identifier: hero_video
|
||||
type: File
|
||||
minitems: 0
|
||||
@@ -115,24 +146,28 @@ fields:
|
||||
type: Checkbox
|
||||
default: 1
|
||||
|
||||
- identifier: hero_layout_variant
|
||||
type: Select
|
||||
renderType: selectSingle
|
||||
default: fullscreen
|
||||
items:
|
||||
- label: Fullscreen
|
||||
value: fullscreen
|
||||
- label: Medium
|
||||
value: md
|
||||
- label: Small
|
||||
value: sm
|
||||
- label: Extra Small
|
||||
value: xs
|
||||
# --- Background video with overlay layer ---
|
||||
- identifier: hero_bgvideo
|
||||
type: File
|
||||
label: Background Video
|
||||
description: 'Fullscreen background video behind the hero content.'
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
|
||||
- identifier: show_logo_wall
|
||||
type: Checkbox
|
||||
default: 0
|
||||
- identifier: hero_overlay_color
|
||||
type: Color
|
||||
label: Overlay Color
|
||||
description: 'Overlay layer on top of the background video/image.'
|
||||
|
||||
- identifier: debug
|
||||
type: Checkbox
|
||||
default: 0
|
||||
- identifier: hero_overlay_opacity
|
||||
type: Number
|
||||
label: 'Overlay Opacity (0–1)'
|
||||
format: decimal
|
||||
default: 0.4
|
||||
range:
|
||||
lower: 0
|
||||
upper: 1
|
||||
slider:
|
||||
step: 0.05
|
||||
width: 200
|
||||
|
||||
@@ -102,6 +102,28 @@
|
||||
<trans-unit id="hero_bgimage.description">
|
||||
<source>Optional background image placed behind the hero content.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab_content.label">
|
||||
<source>Content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab_images.label">
|
||||
<source>Images</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab_video.label">
|
||||
<source>Video</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_bgvideo.label">
|
||||
<source>Background Video</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_bgvideo.description">
|
||||
<source>Fullscreen background video behind the hero content.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_overlay_color.label">
|
||||
<source>Overlay Color</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_overlay_opacity.label">
|
||||
<source>Overlay Opacity (0–1)</source>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
4
packages/vitec/ContentBlocks/ContentElements/quotation/assets/icon.svg
Executable file
4
packages/vitec/ContentBlocks/ContentElements/quotation/assets/icon.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<path d="M6 8h8v8h-6l-2 8H4l2-8V8z" fill="#ff6a00"/>
|
||||
<path d="M18 8h8v8h-6l-2 8h-2l2-8V8z" fill="#0a3d62"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 203 B |
44
packages/vitec/ContentBlocks/ContentElements/quotation/config.yaml
Executable file
44
packages/vitec/ContentBlocks/ContentElements/quotation/config.yaml
Executable file
@@ -0,0 +1,44 @@
|
||||
name: vitec/quotation
|
||||
group: vitec_custom_components
|
||||
prefixFields: true
|
||||
prefixType: vendor
|
||||
|
||||
fields:
|
||||
- identifier: header_section
|
||||
type: Palette
|
||||
label: Header
|
||||
fields:
|
||||
- identifier: header
|
||||
useExistingField: true
|
||||
- type: Linebreak
|
||||
- identifier: header_layout
|
||||
useExistingField: true
|
||||
- identifier: header_position
|
||||
useExistingField: true
|
||||
- identifier: date
|
||||
useExistingField: true
|
||||
- type: Linebreak
|
||||
- identifier: header_link
|
||||
useExistingField: true
|
||||
- type: Linebreak
|
||||
- identifier: subheader
|
||||
useExistingField: true
|
||||
|
||||
- identifier: quote
|
||||
type: Textarea
|
||||
required: true
|
||||
rows: 5
|
||||
|
||||
- identifier: quote_name
|
||||
type: Text
|
||||
max: 120
|
||||
|
||||
- identifier: quote_position
|
||||
type: Text
|
||||
max: 180
|
||||
|
||||
- identifier: quote_logo
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
28
packages/vitec/ContentBlocks/ContentElements/quotation/language/labels.xlf
Executable file
28
packages/vitec/ContentBlocks/ContentElements/quotation/language/labels.xlf
Executable file
@@ -0,0 +1,28 @@
|
||||
<?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 · Customer Quotation</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="description">
|
||||
<source>Customer quote with name, position and optional logo.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="quote.label">
|
||||
<source>Quote</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="quote.description">
|
||||
<source>Plain quote text without quotation marks — the frontend adds them.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="quote_name.label">
|
||||
<source>Name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="quote_position.label">
|
||||
<source>Position / Company</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="quote_logo.label">
|
||||
<source>Logo (optional)</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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">
|
||||
<div class="vitec-preview__thumb-placeholder">“</div>
|
||||
<div class="vitec-preview__body">
|
||||
<div class="vitec-preview__label">VITEC · Customer Quotation</div>
|
||||
<h3 class="vitec-preview__headline">
|
||||
<f:if condition="{data.quote}">
|
||||
<f:then>“{data.quote -> f:format.crop(maxCharacters: 90)}”</f:then>
|
||||
<f:else><em style="color:#c00;">⚠ Quote missing</em></f:else>
|
||||
</f:if>
|
||||
</h3>
|
||||
<f:if condition="{data.quote_name}">
|
||||
<div class="vitec-preview__subline">— {data.quote_name}<f:if condition="{data.quote_position}">, {data.quote_position}</f:if></div>
|
||||
</f:if>
|
||||
</div>
|
||||
<div class="vitec-preview__settings">
|
||||
<f:if condition="{data.quote_logo.0}"><span class="vitec-badge">🖼 Logo</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>
|
||||
36
packages/vitec/Resources/Private/Templates/Preview/Modelcard.html
Executable file
36
packages/vitec/Resources/Private/Templates/Preview/Modelcard.html
Executable file
@@ -0,0 +1,36 @@
|
||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||
|
||||
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
|
||||
|
||||
<div class="vitec-preview">
|
||||
<f:if condition="{imageUrl}">
|
||||
<f:then>
|
||||
<img src="{imageUrl}" class="vitec-preview__thumb" width="200" alt="Card preview"/>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<div class="vitec-preview__thumb-placeholder">▢</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
|
||||
<div class="vitec-preview__body">
|
||||
<div class="vitec-preview__label">VITEC · Card — {typeLabel}</div>
|
||||
<h3 class="vitec-preview__headline">
|
||||
<f:if condition="{title}">
|
||||
<f:then>{title}</f:then>
|
||||
<f:else><em style="color:#c00;">⚠ No {typeLabel} selected</em></f:else>
|
||||
</f:if>
|
||||
</h3>
|
||||
<f:if condition="{subtitle}">
|
||||
<div class="vitec-preview__subline">{subtitle -> f:format.crop(maxCharacters: 90)}</div>
|
||||
</f:if>
|
||||
</div>
|
||||
|
||||
<div class="vitec-preview__settings">
|
||||
<span class="vitec-badge">{typeLabel}</span>
|
||||
<span class="vitec-badge">Layout: {layout}</span>
|
||||
<f:if condition="{recordUid} == 0">
|
||||
<span class="vitec-badge vitec-badge--warning">⚠ nothing selected</span>
|
||||
</f:if>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
@@ -204,4 +204,44 @@
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Small monitors: drop the body text — label, headline and badges stay. */
|
||||
@media (max-width: 1400px) {
|
||||
.vitec-preview__body-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Real fix: react to the PREVIEW COLUMN width, not the monitor width.
|
||||
The page-module body of a CE containing a vitec preview becomes a size
|
||||
container (e.g. inside narrow b13 container columns). */
|
||||
.t3-page-ce-body:has(.vitec-preview) {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
/* Narrow column: stack the grid, drop the body text, badges in a row. */
|
||||
@container (max-width: 700px) {
|
||||
.vitec-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.vitec-preview__body-text {
|
||||
display: none;
|
||||
}
|
||||
.vitec-preview__settings {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.vitec-preview__thumb,
|
||||
.vitec-preview__thumb-placeholder {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very narrow column: only label, headline and badges survive. */
|
||||
@container (max-width: 420px) {
|
||||
.vitec-preview__subline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg
Executable file
6
packages/vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect x="3" y="7" width="12" height="8" rx="1.5" fill="#ff6a00"/>
|
||||
<rect x="17" y="7" width="12" height="8" rx="1.5" fill="#9aa2ab"/>
|
||||
<rect x="3" y="17" width="12" height="8" rx="1.5" fill="#9aa2ab"/>
|
||||
<rect x="17" y="17" width="12" height="8" rx="1.5" fill="#9aa2ab"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
4
packages/vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg
Executable file
4
packages/vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<path d="M16 3c-5 0-9 4-9 9 0 6.6 9 17 9 17s9-10.4 9-17c0-5-4-9-9-9z" fill="#ff6a00"/>
|
||||
<circle cx="16" cy="12" r="4" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
6
packages/vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg
Executable file
6
packages/vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect x="5" y="4" width="22" height="24" rx="2" fill="#0a3d62"/>
|
||||
<rect x="8" y="7" width="16" height="9" rx="1" fill="#ff6a00"/>
|
||||
<rect x="8" y="19" width="16" height="2.5" rx="1" fill="#fff"/>
|
||||
<rect x="8" y="23" width="10" height="2.5" rx="1" fill="#9aa2ab"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -157,4 +157,40 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1750000000] = [
|
||||
\Evomedien\Vitec\Controller\EventController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Locationlist',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\LocationController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\LocationController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Customerlogos',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\CustomerController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\CustomerController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Modelcard',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\ModelcardController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\ModelcardController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
})();
|
||||
|
||||
@@ -100,7 +100,7 @@ CREATE TABLE tx_vitec_domain_model_usecase (
|
||||
card_image int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
customer_logo int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
teaser text,
|
||||
market int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
markets int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
|
||||
hero_bgimage int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
hero_small_image int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
@@ -243,3 +243,41 @@ CREATE TABLE tx_vitec_usecase_product_mm (
|
||||
KEY uid_local (uid_local),
|
||||
KEY uid_foreign (uid_foreign)
|
||||
);
|
||||
|
||||
CREATE TABLE tx_vitec_usecase_market_mm (
|
||||
uid_local int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
uid_foreign int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
sorting int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
sorting_foreign int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
KEY uid_local (uid_local),
|
||||
KEY uid_foreign (uid_foreign)
|
||||
);
|
||||
|
||||
CREATE TABLE tx_vitec_domain_model_location (
|
||||
title varchar(255) DEFAULT '' NOT NULL,
|
||||
slug varchar(255) DEFAULT '' NOT NULL,
|
||||
country_code varchar(5) DEFAULT '' NOT NULL,
|
||||
latitude decimal(11,7) DEFAULT '0.0000000' NOT NULL,
|
||||
longitude decimal(11,7) DEFAULT '0.0000000' NOT NULL,
|
||||
address_company varchar(255) DEFAULT '' NOT NULL,
|
||||
street varchar(255) DEFAULT '' NOT NULL,
|
||||
address_additional varchar(255) DEFAULT '' NOT NULL,
|
||||
postal_code varchar(20) DEFAULT '' NOT NULL,
|
||||
city varchar(255) DEFAULT '' NOT NULL,
|
||||
region varchar(255) DEFAULT '' NOT NULL,
|
||||
country varchar(255) DEFAULT '' NOT NULL,
|
||||
phone varchar(100) DEFAULT '' NOT NULL,
|
||||
fax varchar(100) DEFAULT '' NOT NULL,
|
||||
email varchar(255) DEFAULT '' NOT NULL,
|
||||
contact_link varchar(1024) DEFAULT '' NOT NULL,
|
||||
legal_links text,
|
||||
marker_label varchar(255) DEFAULT '' NOT NULL,
|
||||
marker_color varchar(20) DEFAULT '#ff6633' NOT NULL,
|
||||
marker_size decimal(3,2) DEFAULT '0.50' NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tx_vitec_domain_model_customer (
|
||||
title varchar(255) DEFAULT '' NOT NULL,
|
||||
logo int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
emphasize_logo smallint(5) unsigned DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user