product.features: Why-choose cards with icons (contract 0036-FE)
- new inline table tx_vitec_product_feature (title, cardtext, FAL icon)
+ `features` field on the product record; ProductShowJsonRenderer
emits product.features {heading, items[{title, text, icon}]} and null
while a product has no cards, so the front end keeps its
applications-HTML fallback
- new vitec:seed-product-features: seeds the cards from the
applications <p><strong> pairs and matches the staged agency icons by
normalised title (camelCase split, stop words, Levenshtein <= 3 typo
tolerance); products with existing cards are never touched
- new vitec:build-why-vitec-bands: "Why VITEC?" intro + icon_card band
on the 7 overview pages with a staged icon set (the contract's
"centered" variant does not exist - documented in ticket 0039-BE);
vision/visionav sub-group sets stay unassigned
- product-check page: sortable title column and an Open Product button
- new migration fix_colombia_quote.php: stray quote element #12037
removed, quote_role set (ticket 0038-FE)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ccb9b149e
commit
46b12d4a58
8 files changed
+799
-4
No files matched your search
@@ -258,6 +258,11 @@ class ProductShowJsonRenderer
|
||||
'resources' => $this->resourcesBand($uid, (string)(($product['resources_layout'] ?? '') ?: '50-50')),
|
||||
];
|
||||
|
||||
// "Why choose <product>?" cards (contract 0036-FE): null while the
|
||||
// product has no feature records - the front end then keeps parsing
|
||||
// the `applications` HTML above.
|
||||
$data['features'] = $this->featuresBand($uid, (string)$data['applications'], (string)$data['title']);
|
||||
|
||||
// schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit
|
||||
// @graph string. The frontend injects it as <script type="application/ld+json">.
|
||||
// Supersedes the manual `structureddata` field above (kept for backwards
|
||||
@@ -267,6 +272,67 @@ class ProductShowJsonRenderer
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Why choose" card band: editor-maintained tx_vitec_product_feature
|
||||
* records in sorting order, icons resolved to urls. The heading is the
|
||||
* first <h3> of the applications HTML, falling back to the generic
|
||||
* "Why choose <title>?".
|
||||
*
|
||||
* @return array{heading:string,items:array<int,array<string,mixed>>}|null
|
||||
*/
|
||||
protected function featuresBand(int $productUid, string $applicationsHtml, string $productTitle): ?array
|
||||
{
|
||||
try {
|
||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$rows = $pool->getConnectionForTable('tx_vitec_product_feature')->fetchAllAssociative(
|
||||
'SELECT uid, title, cardtext FROM tx_vitec_product_feature
|
||||
WHERE deleted = 0 AND hidden = 0 AND product = ? ORDER BY sorting, uid',
|
||||
[$productUid]
|
||||
);
|
||||
if ($rows === []) {
|
||||
return null;
|
||||
}
|
||||
$icons = [];
|
||||
foreach ($pool->getConnectionForTable('sys_file_reference')->fetchAllAssociative(
|
||||
"SELECT r.uid_foreign, r.alternative, f.identifier FROM sys_file_reference r
|
||||
JOIN sys_file f ON f.uid = r.uid_local
|
||||
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_product_feature'
|
||||
AND r.fieldname = 'icon'
|
||||
AND r.uid_foreign IN (" . implode(',', array_map('intval', array_column($rows, 'uid'))) . ')
|
||||
ORDER BY r.uid_foreign, r.sorting_foreign'
|
||||
) as $iconRow) {
|
||||
$icons[(int)$iconRow['uid_foreign']] ??= [
|
||||
'url' => '/fileadmin' . (string)$iconRow['identifier'],
|
||||
'alternative' => trim((string)($iconRow['alternative'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
$heading = '';
|
||||
if (preg_match('#<h3[^>]*>(.*?)</h3>#is', $applicationsHtml, $m)) {
|
||||
$heading = trim(strip_tags($m[1]));
|
||||
}
|
||||
if ($heading === '') {
|
||||
$heading = 'Why choose ' . $productTitle . '?';
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($rows as $row) {
|
||||
$icon = $icons[(int)$row['uid']] ?? null;
|
||||
if ($icon !== null && $icon['alternative'] === '') {
|
||||
$icon['alternative'] = (string)$row['title'];
|
||||
}
|
||||
$items[] = [
|
||||
'title' => (string)$row['title'],
|
||||
'text' => trim((string)($row['cardtext'] ?? '')),
|
||||
'icon' => $icon,
|
||||
];
|
||||
}
|
||||
return ['heading' => $heading, 'items' => $items];
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the product's structured-data @graph from the already-serialized
|
||||
* product array. Exception-safe: returns '' on any failure.
|
||||
|
||||
Reference in new issue
Block a user