Products menu: single-product overviews link to the product (spec v1.33)
Decision Harry 2026-11-12 for the third level of the products panel: an overview page with exactly one product is presented as that product. items[].link becomes the product detail page and items[].image the product picture (productimage, else heroimage, else the page media as before); new additive fields items[].linkType (product | overview) and items[].overviewLink (always the overview page). The product uid is resolved from the card link via the product slug; one extra query per build. Applies today to Avedia, EZ TV, X-Player End-Points, ChannelLink IP Gateway and ActiveSQX and switches back automatically once an editor adds a second card. Removing the sub-product line and the shortcut pill/subline is front end work (ticket 0121-BE). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8031247acf
commit
4f47382679
2 files changed
+80
-7
No files matched your search
@@ -29,6 +29,14 @@ use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
* pages the visitor reaches from the overview page; the menu does not show
|
||||
* them, the count and the list serve the "N sub-products" line and search.
|
||||
*
|
||||
* An overview page with exactly ONE product is shown as that product
|
||||
* (decision Harry 2026-11-12, e.g. Avedia, EZ TV, X-Player, ChannelLink,
|
||||
* ActiveSQX): `link` is the product detail page and `image` the product's
|
||||
* own image (productimage, else heroimage, else the page media). `linkType`
|
||||
* says which case it is (`product` | `overview`), `overviewLink` always
|
||||
* keeps the overview page. The page itself stays reachable (breadcrumb,
|
||||
* search, sitemap).
|
||||
*
|
||||
* A and B were sysfolders (menu labels only) until 2026-11-05; Olli: every
|
||||
* category and sub-category is a clickable page. Both levels now carry
|
||||
* `link` (additive; '' while a level is still a folder). Folders are still
|
||||
@@ -119,16 +127,21 @@ final class ProvideProductCatalog
|
||||
$uid = (int)$itemRow['uid'];
|
||||
$pagePos[$uid] = [$c, $g, $p];
|
||||
$products = $productsByPage[$uid] ?? [];
|
||||
// exactly one product: the tile is that product (link + image), see class comment
|
||||
$single = count($products) === 1 ? $products[0] : null;
|
||||
$item = [
|
||||
'type' => 'page',
|
||||
'uid' => $uid,
|
||||
'title' => (string)$itemRow['title'],
|
||||
'link' => (string)$itemRow['slug'],
|
||||
'link' => $single !== null ? $single['link'] : (string)$itemRow['slug'],
|
||||
'linkType' => $single !== null ? 'product' : 'overview',
|
||||
'overviewLink' => (string)$itemRow['slug'],
|
||||
'productCount' => count($products),
|
||||
'products' => $products,
|
||||
'products' => array_map(static fn(array $p): array => ['title' => $p['title'], 'link' => $p['link']], $products),
|
||||
];
|
||||
if ($stage >= 2) {
|
||||
$item['image'] = $this->pageImage($uid);
|
||||
$item['image'] = ($single !== null && $single['uid'] > 0 ? $this->productImage($single['uid']) : null)
|
||||
?? $this->pageImage($uid);
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
@@ -228,7 +241,7 @@ final class ProvideProductCatalog
|
||||
* product detail page (container children included), creation order.
|
||||
*
|
||||
* @param int[] $pageUids
|
||||
* @return array<int,array<int,array{title:string,link:string}>> page uid => products
|
||||
* @return array<int,array<int,array{title:string,link:string,uid:int}>> page uid => products
|
||||
*/
|
||||
private function productsOnPages(array $pageUids): array
|
||||
{
|
||||
@@ -244,16 +257,49 @@ final class ProvideProductCatalog
|
||||
)
|
||||
->orderBy('uid')
|
||||
->executeQuery()->fetchAllAssociative();
|
||||
$uidBySlug = $rows === [] ? [] : $this->productUidsBySlug();
|
||||
$byPage = [];
|
||||
foreach ($rows as $row) {
|
||||
$link = (string)$row['vitec_primary_cta'];
|
||||
$slug = trim((string)parse_url($link, PHP_URL_PATH), '/');
|
||||
$slug = substr($slug, strlen('product/'));
|
||||
$byPage[(int)$row['pid']][] = [
|
||||
'title' => (string)$row['header'],
|
||||
'link' => (string)$row['vitec_primary_cta'],
|
||||
'link' => $link,
|
||||
'uid' => $uidBySlug[$slug] ?? 0,
|
||||
];
|
||||
}
|
||||
return $byPage;
|
||||
}
|
||||
|
||||
/** @return array<string,int> product slug (without slashes) => uid, live products only */
|
||||
private function productUidsBySlug(): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||
$rows = $queryBuilder->select('uid', 'slug')->from('tx_vitec_domain_model_product')
|
||||
->executeQuery()->fetchAllAssociative();
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[trim((string)$row['slug'], '/')] = (int)$row['uid'];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/** The product's own picture (productimage, else heroimage) as 640px tile - or null. */
|
||||
private function productImage(int $productUid): ?array
|
||||
{
|
||||
try {
|
||||
foreach (['productimage', 'heroimage'] as $field) {
|
||||
$references = $this->fileRepository->findByRelation('tx_vitec_domain_model_product', $field, $productUid);
|
||||
if ($references !== []) {
|
||||
return $this->tile($references[0]);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** First page media (page properties > resources), 640px tile, root-relative - or null. */
|
||||
private function pageImage(int $pageUid): ?array
|
||||
{
|
||||
@@ -262,6 +308,17 @@ final class ProvideProductCatalog
|
||||
if ($references === []) {
|
||||
return null;
|
||||
}
|
||||
return $this->tile($references[0]);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{url:string,alternative:string,width:int,height:int}|null */
|
||||
private function tile(\TYPO3\CMS\Core\Resource\FileReference $reference): ?array
|
||||
{
|
||||
try {
|
||||
$references = [$reference];
|
||||
$imageService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ImageService::class);
|
||||
$processed = $imageService->applyProcessingInstructions($references[0], ['width' => '640', 'fileExtension' => 'webp']);
|
||||
$url = $imageService->getImageUri($processed);
|
||||
|
||||
Reference in new issue
Block a user