feat: snow reports on TYPO3 13, with a page type for the static pages

The snow report list ("Schneehöhen" plugin) is ported from TYPO3 10:
SnowreportController, SnowreportRepository::getSnowreportsForRegions(),
the plugin registration with its flexform (regions) and the template,
which now formats dates with d.m.Y.

Page type 1705 (/<page>/snowreport) renders only the snow report plugins
of a page, like /form for forms. The static pages of the hybrid setup can
load it to replace the snow reports frozen at export time; each list
carries id="snowreport-<content uid>" for hx-select, since TYPO3 10 gives
the plugin no element of its own. ep:import-snowreport already flushes
the page cache after each import. The suffix is in the PageType enhancer
of every site with snow reports.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

feat: the snow report box includes the spacing of its content element

id="snowreport-<content uid>" moves from the list to a wrapper around
ep:container. The static pages of the hybrid setup no longer export the
snow reports at all, only an empty placeholder that loads the whole box,
spacing included, from /<page>/snowreport, so a page never shows
outdated snow depths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-22 14:19:53 +02:00
1 parent 62f3bee230
commit ca2fdc1e13
12 files changed
+211 -1

No files matched your search

+1
View File
@@ -284,6 +284,7 @@ routeEnhancers:
/html: 1702
/form: 1704
/captcha: 3413
/snowreport: 1705
.json: 1803
.ical: 1710
sitemap.xml: 1533906435
+2 -1
View File
@@ -79,7 +79,7 @@ routeEnhancers:
hotel_uid: \d+
date_from: '\d{4}\-\d{2}\-\d{2}'
date_to: '\d{4}\-\d{2}\-\d{2}'
# Only for the form and captcha page types; default '' keeps the URLs without trailing slash
# Only for the form, captcha and snow report page types; default '' keeps the URLs without trailing slash
PageTypeSuffix:
type: PageType
default: ''
@@ -87,6 +87,7 @@ routeEnhancers:
map:
/form: 1704
/captcha: 3413
/snowreport: 1705
routes:
-
route: robots.txt
+1
View File
@@ -68,6 +68,7 @@ routeEnhancers:
/html: 1702
/form: 1704
/captcha: 3413
/snowreport: 1705
sitemap.xml: 1533906435
yoast-snippetpreview.json: 1480321830
routes:
+1
View File
@@ -52,6 +52,7 @@ routeEnhancers:
/html: 1702
/form: 1704
/captcha: 3413
/snowreport: 1705
sitemap.xml: 1533906435
yoast-snippetpreview.json: 1480321830
routes:
+1
View File
@@ -52,6 +52,7 @@ routeEnhancers:
/html: 1702
/form: 1704
/captcha: 3413
/snowreport: 1705
sitemap.xml: 1533906435
yoast-snippetpreview.json: 1480321830
routes:
@@ -0,0 +1,23 @@
<?php
namespace EP\EpProducts\Controller;
use EP\EpProducts\Domain\Repository\SnowreportRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class SnowreportController extends ActionController
{
public function __construct(private readonly SnowreportRepository $snowreportRepository)
{
}
public function listAction(): ResponseInterface
{
$regionUids = GeneralUtility::intExplode(',', (string)($this->settings['regionUids'] ?? ''), true);
$this->view->assign('data', $this->request->getAttribute('currentContentObject')?->data ?? []);
$this->view->assign('list', $this->snowreportRepository->getSnowreportsForRegions($regionUids));
return $this->htmlResponse();
}
}
@@ -0,0 +1,43 @@
<?php
namespace EP\EpProducts\Domain\Repository;
use TYPO3\CMS\Core\Database\Connection;
class SnowreportRepository extends AbstractRepository
{
/**
* Snow reports of the given regions (all regions with a snow report if empty), one per vendor
* record, with region and country data for the list.
*/
public function getSnowreportsForRegions(array $regionUids = []): array
{
$qb = $this->getDbConnection()->createQueryBuilder();
$qb
->select(
'snowreport.*',
'region.name as regionName', 'region.uid as regionUid', 'region.detail_page as regionDetailPage',
'country.name as countryName', 'country.uid as countryUid', 'country.code as countryCode',
'country.detail_page as countryDetailPage',
'season_start as seasonStart', 'season_end as seasonEnd'
)
->addSelectLiteral('(UNIX_TIMESTAMP() >= snowreport.season_start AND UNIX_TIMESTAMP() <= snowreport.season_end) as inSeason')
->from('tx_epproducts_domain_model_snowreport', 'snowreport')
->innerJoin('snowreport', 'tx_epproducts_domain_model_region', 'region', 'snowreport.uid = region.snowreport')
->innerJoin('region', 'tx_epproducts_domain_model_country', 'country', 'region.country = country.uid')
->where('region.snowreport > 0')
->groupBy('snowreport.vendor_uid')
->orderBy('region.name')
;
if ($regionUids) {
$qb->andWhere($qb->expr()->in(
'region.uid',
$qb->createNamedParameter($regionUids, Connection::PARAM_INT_ARRAY)
));
}
return $qb->executeQuery()->fetchAllAssociative();
}
}
@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<T3DataStructure>
<meta>
<langDisable>1</langDisable>
</meta>
<sheets>
<sDEF>
<ROOT>
<sheetTitle>Konfiguration</sheetTitle>
<type>array</type>
<el>
<settings.regionUids>
<label>Gebiete einschränken</label>
<config>
<type>select</type>
<renderType>selectMultipleSideBySide</renderType>
<foreign_table>tx_epproducts_domain_model_region</foreign_table>
<foreign_table_where>AND tx_epproducts_domain_model_region.snowreport > 0 ORDER BY name</foreign_table_where>
<minitems>0</minitems>
<maxitems>99</maxitems>
<size>10</size>
</config>
</settings.regionUids>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>
@@ -43,6 +43,12 @@ call_user_func(function () {
'Buchungstabelle'
);
ExtensionUtility::registerPlugin(
'EpProducts',
'snowreport_list',
'Schneehöhen'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_product_detail'] = 'pi_flexform';
ExtensionManagementUtility::addPiFlexFormValue(
'epproducts_product_detail',
@@ -61,4 +67,10 @@ call_user_func(function () {
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_pricetable.xml'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_snowreport_list'] = 'pi_flexform';
ExtensionManagementUtility::addPiFlexFormValue(
'epproducts_snowreport_list',
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_snowreport.xml'
);
});
+9
View File
@@ -2,6 +2,7 @@
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use EP\EpProducts\Controller\ProductController;
use EP\EpProducts\Controller\SearchController;
use EP\EpProducts\Controller\SnowreportController;
use EP\EpProducts\Controller\TravelinfoController;
use EP\EpProducts\Controller\WatchlistController;
use EP\EpProducts\Controller\AjaxFilterPanelController;
@@ -81,6 +82,14 @@ $boot = function () {
]
);
ExtensionUtility::configurePlugin(
'EpProducts',
'snowreport_list',
[
SnowreportController::class => 'list',
]
);
ExtensionUtility::configurePlugin(
'EpProducts',
'ajax',
@@ -0,0 +1,29 @@
# The snow reports of a page without the page around them (/path/snowreport). The static pages of
# the hybrid setup load this with htmx and replace the snow reports of the export with it: the
# page URL itself is a static file that never reaches TYPO3, and the snow reports are imported
# daily (ep:import-snowreport, which flushes the page cache). Each list carries
# id="snowreport-<content uid>" (Templates/Snowreport/List.html) for hx-select.
tx_eptheme_snowreport = PAGE
tx_eptheme_snowreport {
typeNum = 1705
config {
disableAllHeaderCode = 1
disablePrefixComment = 1
additionalHeaders.10.header = Content-Type: text/html; charset=utf-8
additionalHeaders.20.header = X-Robots-Tag: noindex, nofollow
admPanel = 0
debug = 0
}
10 = CONTENT
10 {
table = tt_content
select {
where = {#CType} = 'list' AND {#list_type} = 'epproducts_snowreport_list'
orderBy = sorting
}
# Only the plugin: header and frame stay on the static page
renderObj =< tt_content.list.20.epproducts_snowreport_list
}
}
@@ -0,0 +1,61 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default"/>
<f:section name="Main">
<f:comment>
The id lets the static pages of the hybrid setup pick this list, with the spacing of
ep:container, out of the snow report page type (/path/snowreport), which renders every
snow report plugin of a page.
</f:comment>
<div id="snowreport-{data.uid}">
<ep:container data="{data}">
<div class="flex flex-col space-y-2">
<f:for each="{list}" as="snowreport">
<div class="grid md:grid-cols-3 md:gap-y-0">
<div class="md:col-span-1 bg-ep-primary-dark p-2 flex items-start space-x-2">
{ep:icon(icon: 'flag-{snowreport.countryCode -> f:format.case(mode: \'lower\')}', class: 'w-8 h-6')}
<f:link.typolink parameter="{snowreport.regionDetailPage}"
title="{snowreport.regionName}"
class="text-white uppercase text-lg">
{snowreport.regionName}
</f:link.typolink>
</div>
<div class="md:col-span-2 bg-ep-primary-dark/20 p-2">
<f:if condition="{snowreport.inSeason}">
<f:then>
<ul class="flex flex-col space-y-1">
<li>
<strong>Berg/Tal:</strong> {snowreport.snow_level_mountain -> v:or(alternative: '-')}/{snowreport.snow_level_valley -> v:or(alternative: '-')} cm
</li>
<li>
<strong>Qualität:</strong> {snowreport.snow_quality}
</li>
<li>
<strong>Geöffnete Lifte:</strong> <f:if condition="{snowreport.lifts_opened} > 0"><f:then>{snowreport.lifts_opened}/{snowreport.lifts_total}</f:then><f:else>keine Angabe</f:else></f:if>
</li>
<li>
<strong>Letzter Schneefall:</strong> {f:if(condition: snowreport.last_snowfall, then: '{snowreport.last_snowfall -> f:format.date(format: \'d.m.Y\')}', else: 'keine Angabe')}
</li>
<li>
<strong>Letzte Akt.:</strong> {snowreport.last_update -> f:format.date(format: 'd.m.Y')}
</li>
</ul>
</f:then>
<f:else>
Saison vom {snowreport.seasonStart -> f:format.date(format: 'd.m.Y')} bis {snowreport.seasonEnd -> f:format.date(format: 'd.m.Y')}
</f:else>
</f:if>
</div>
</div>
</f:for>
</div>
</ep:container>
</div>
</f:section>
</html>