Merge pull request 'Zeitzone korrigiert' (#20) from develop into main

Reviewed-on: https://git.feigel.it/SFBadSteben/schachfreunde-badsteben/pulls/20
This commit is contained in:
2025-09-06 12:29:00 +02:00
3 changed files with 339 additions and 174 deletions
+79 -3
View File
@@ -1,5 +1,5 @@
<?php
<?php
// URL der öffentlichen ICS-Datei // URL der öffentlichen ICS-Datei
define( define(
'CAL_URL', 'CAL_URL',
@@ -20,6 +20,69 @@ function format_ics_date($date)
} }
} }
function format_ics_date_with_timezone($date, $timezone = null)
{
// Unterstützt sowohl ganztägige als auch Zeitangaben
if (strpos($date, 'T') !== false) {
$dt = null;
// Prüfe ob es eine UTC-Zeit ist (endet mit Z)
if (substr($date, -1) === 'Z') {
// UTC-Zeit: 20250405T130000Z
$utc_date = substr($date, 0, -1); // Entferne das Z
$dt = DateTime::createFromFormat('Ymd\THis', $utc_date, new DateTimeZone('UTC'));
} elseif ($timezone) {
// Zeitzone angegeben: 20250405T130000 mit TZID
try {
$dt = new DateTime($date, new DateTimeZone($timezone));
} catch (Exception $e) {
// Fallback auf lokale Zeit
$dt = DateTime::createFromFormat('Ymd\THis', substr($date, 0, 15));
}
} else {
// Lokale Zeit ohne Zeitzone
$dt = DateTime::createFromFormat('Ymd\THis', substr($date, 0, 15));
}
if ($dt) {
// ISO 8601 Format für JavaScript
$iso_date = $dt->format('Y-m-d\TH:i:sP'); // P = Zeitzone
$display_date = $dt->format('d.m.Y H:i');
return [
'iso' => $iso_date,
'display' => $display_date,
'has_time' => true,
];
}
return [
'iso' => $date,
'display' => $date,
'has_time' => true,
];
} else {
$dt = DateTime::createFromFormat('Ymd', $date);
if ($dt) {
$iso_date = $dt->format('Y-m-d');
$display_date = $dt->format('d.m.Y');
return [
'iso' => $iso_date,
'display' => $display_date,
'has_time' => false,
];
}
return [
'iso' => $date,
'display' => $date,
'has_time' => false,
];
}
}
// ICS-Datei laden // ICS-Datei laden
$ics = @file_get_contents(CAL_URL); $ics = @file_get_contents(CAL_URL);
if (!$ics) { if (!$ics) {
@@ -49,9 +112,21 @@ function parse_ics($ics)
if (count($parts) === 2) { if (count($parts) === 2) {
$key = $parts[0]; $key = $parts[0];
$val = $parts[1]; $val = $parts[1];
// Zeitzoneninformation extrahieren
$timezone = null;
if (preg_match('/TZID=([^:;]+)/', $key, $matches)) {
$timezone = $matches[1];
}
// Nur den eigentlichen Property-Namen extrahieren // Nur den eigentlichen Property-Namen extrahieren
$key = strtoupper(preg_replace('/;.+$/', '', $key)); $cleanKey = strtoupper(preg_replace('/;.+$/', '', $key));
$event[$key] = $val; $event[$cleanKey] = $val;
// Zeitzoneninformation separat speichern
if ($timezone && in_array($cleanKey, ['DTSTART', 'DTEND'])) {
$event[$cleanKey . '_TZID'] = $timezone;
}
} }
} }
} }
@@ -64,3 +139,4 @@ $events = parse_ics($ics);
return function () use ($events) { return function () use ($events) {
return $events; return $events;
}; };
+58 -6
View File
@@ -2,7 +2,7 @@
$events = collection('termine'); $events = collection('termine');
// Aktuelles Datum // Aktuelles Datum
$today = (new DateTime())->format('Ymd'); $today = new DateTime()->format('Ymd');
// Nur zukünftige Termine anzeigen // Nur zukünftige Termine anzeigen
$future_events = array_filter($events, function ($event) use ($today) { $future_events = array_filter($events, function ($event) use ($today) {
@@ -48,10 +48,13 @@
$summary = $event['SUMMARY'] ?? ''; $summary = $event['SUMMARY'] ?? '';
$location = $event['LOCATION'] ?? ''; $location = $event['LOCATION'] ?? '';
$desc = $event['DESCRIPTION'] ?? ''; $desc = $event['DESCRIPTION'] ?? '';
$date = format_ics_date($start); $timezone = $event['DTSTART_TZID'] ?? null;
$time = (str_contains($start, 'T')) ? substr($date, 11) : 'ganztägig'; $date_info = format_ics_date_with_timezone($start, $timezone);
$date = $date_info['display'];
$time = $date_info['has_time'] ? substr($date, 11) : 'ganztägig';
$day = substr($date, 0, 2); $day = substr($date, 0, 2);
$month = substr($date, 3, 2); $month = substr($date, 3, 2);
$iso_date = $date_info['iso'];
?> ?>
<div class="termine-card flex-none w-full md:w-1/2 bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl transform hover:scale-102 transition-all duration-300 ease-in-out border border-blue-200"> <div class="termine-card flex-none w-full md:w-1/2 bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl transform hover:scale-102 transition-all duration-300 ease-in-out border border-blue-200">
@@ -66,7 +69,9 @@
<!-- Datumsanzeige --> <!-- Datumsanzeige -->
<div class="flex-grow flex items-center justify-center"> <div class="flex-grow flex items-center justify-center">
<div class="text-3xl font-bold text-sf_grau-800" data-day><?php echo htmlspecialchars($day); ?></div> <div class="text-3xl font-bold text-sf_grau-800" data-day><?php echo htmlspecialchars(
$day,
); ?></div>
</div> </div>
<!-- Dekorative Elemente - kleine Punkte für Kalendertage --> <!-- Dekorative Elemente - kleine Punkte für Kalendertage -->
@@ -81,8 +86,16 @@
</div> </div>
</div> </div>
<div class="ml-5 flex-grow"> <div class="ml-5 flex-grow">
<pack class="text-xl font-medium text-sf_grau-900"><?= $event["SUMMARY"] ?></p> <pack class="text-xl font-medium text-sf_grau-900"><?= $event['SUMMARY'] ?></p>
<p class="text-sf_blau-700 text-lg font-medium mt-1">ab <?php echo htmlspecialchars($time); ?> Uhr</p> <p class="text-sf_blau-700 text-lg font-medium mt-1">
<?php if ($date_info['has_time']): ?>
ab <span class="local-time" data-iso-date="<?php echo htmlspecialchars(
$iso_date,
); ?>"><?php echo htmlspecialchars($time); ?></span> Uhr
<?php else: ?>
ganztägig
<?php endif; ?>
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -169,6 +182,45 @@
// Initialer Aufruf // Initialer Aufruf
updateArrowState(); updateArrowState();
// Zeitzonenkonvertierung für lokale Zeiten
function convertTimesToLocal() {
const timeElements = document.querySelectorAll('.local-time');
timeElements.forEach(element => {
const isoDate = element.getAttribute('data-iso-date');
if (isoDate) {
try {
// Erstelle ein Date-Objekt aus der ISO-Date
// JavaScript erkennt automatisch UTC-Zeiten (mit Z oder +00:00)
const date = new Date(isoDate);
// Prüfe ob das Datum gültig ist
if (isNaN(date.getTime())) {
console.warn('Ungültiges Datum:', isoDate);
return;
}
// Formatiere die Zeit in der lokalen Zeitzone des Browsers
const localTime = date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
// Aktualisiere den Text
element.textContent = localTime;
// Debug-Information (kann später entfernt werden)
console.log('Konvertiert:', isoDate, '->', localTime, 'in Zeitzone:', Intl.DateTimeFormat().resolvedOptions().timeZone);
} catch (error) {
console.warn('Fehler bei der Zeitzonenkonvertierung:', error, 'für Datum:', isoDate);
}
}
});
}
// Zeitzonenkonvertierung beim Laden der Seite
convertTimesToLocal();
// Responsives Verhalten - bei Fenstergröße-Änderung // Responsives Verhalten - bei Fenstergröße-Änderung
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
// Anzahl der Karten je nach Bildschirmgröße anpassen // Anzahl der Karten je nach Bildschirmgröße anpassen
+84 -47
View File
@@ -12,7 +12,7 @@ usort($events, function ($a, $b) {
}); });
// Aktuelles Datum // Aktuelles Datum
$today = (new DateTime())->format('Ymd'); $today = new DateTime()->format('Ymd');
// Nur zukünftige Termine anzeigen // Nur zukünftige Termine anzeigen
$future_events = array_filter($events, function ($event) use ($today) { $future_events = array_filter($events, function ($event) use ($today) {
@@ -85,54 +85,43 @@ if ($filter_jahr && $filter_monat) {
<div class="bg-white rounded-lg shadow p-4"> <div class="bg-white rounded-lg shadow p-4">
<h2 class="text-lg font-semibold mb-3">Termine nach Jahr/Monat</h2> <h2 class="text-lg font-semibold mb-3">Termine nach Jahr/Monat</h2>
<ul class="space-y-1"> <ul class="space-y-1">
<?php <?php foreach ($all_events_grouped as $year => $months): ?>
foreach ($all_events_grouped as $year => $months): ?>
<li class="mb-2"> <li class="mb-2">
<div class="flex items-center"> <div class="flex items-center">
<a href="?jahr=<?php <a href="?jahr=<?php echo $year; ?>"
echo $year; ?>"
class="font-bold text-sf_blau-600 focus:outline-none flex items-center group<?php class="font-bold text-sf_blau-600 focus:outline-none flex items-center group<?php
if ($filter_jahr === $year && !$filter_monat) { if ($filter_jahr === $year && !$filter_monat) {
echo ' underline'; echo ' underline';
} ?><?php }
if ($filter_jahr === $year && !$filter_monat) { if ($filter_jahr === $year && !$filter_monat) {
echo ' selected'; echo ' selected';
} ?>" onclick="event.stopPropagation(); openYear('<?php }
echo $year; ?>')"> ?>" onclick="event.stopPropagation(); openYear('<?php echo $year; ?>')">
<span><?php <span><?php echo $year; ?></span>
echo $year; ?></span>
</a> </a>
<button type="button" class="ml-1 focus:outline-none" onclick="toggleYear('<?php <button type="button" class="ml-1 focus:outline-none" onclick="toggleYear('<?php echo $year; ?>')">
echo $year; ?>')"> <svg class="w-4 h-4 transition-transform" id="arrow-<?php echo $year; ?>" fill="none" stroke="currentColor" stroke-width="2"
<svg class="w-4 h-4 transition-transform" id="arrow-<?php
echo $year; ?>" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/> <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg> </svg>
</button> </button>
</div> </div>
<ul class="ml-4 mt-1 hidden" id="months-<?php <ul class="ml-4 mt-1 hidden" id="months-<?php echo $year; ?>">
echo $year; ?>"> <?php foreach ($months as $month => $evts): ?>
<?php
foreach ($months as $month => $evts): ?>
<li> <li>
<a href="?jahr=<?php <a href="?jahr=<?php echo $year; ?>&monat=<?php echo $month; ?>" class="text-sf_blau-500 hover:underline<?php if (
echo $year; ?>&monat=<?php $filter_jahr === $year &&
echo $month; ?>" class="text-sf_blau-500 hover:underline<?php $filter_monat === $month
if ($filter_jahr === $year && $filter_monat === $month) { ) {
echo ' font-bold underline selected'; echo ' font-bold underline selected';
} ?>"> } ?>">
<?php <?php echo $de_months[$month]; ?> (<?php echo count($evts); ?>)
echo $de_months[$month]; ?> (<?php
echo count($evts); ?>)
</a> </a>
</li> </li>
<?php <?php endforeach; ?>
endforeach; ?>
</ul> </ul>
</li> </li>
<?php <?php endforeach; ?>
endforeach; ?>
</ul> </ul>
</div> </div>
<script> <script>
@@ -160,11 +149,9 @@ if ($filter_jahr && $filter_monat) {
</aside> </aside>
<!-- Hauptinhalt Termine --> <!-- Hauptinhalt Termine -->
<div class="md:w-3/4 w-full"> <div class="md:w-3/4 w-full">
<?php <?php if (empty($filtered_events)): ?>
if (empty($filtered_events)): ?>
<div class="text-gray-500">Keine Termine gefunden.</div> <div class="text-gray-500">Keine Termine gefunden.</div>
<?php <?php else: ?>
else: ?>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full border border-gray-200 bg-white rounded-lg shadow"> <table class="min-w-full border border-gray-200 bg-white rounded-lg shadow">
<thead> <thead>
@@ -175,33 +162,40 @@ if ($filter_jahr && $filter_monat) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php <?php foreach ($filtered_events as $event): ?>
foreach ($filtered_events as $event): ?>
<?php <?php
$start = $event['DTSTART'] ?? ''; $start = $event['DTSTART'] ?? '';
$end = $event['DTEND'] ?? ''; $end = $event['DTEND'] ?? '';
$summary = $event['SUMMARY'] ?? ''; $summary = $event['SUMMARY'] ?? '';
$location = $event['LOCATION'] ?? ''; $location = $event['LOCATION'] ?? '';
$desc = $event['DESCRIPTION'] ?? ''; $desc = $event['DESCRIPTION'] ?? '';
$date = format_ics_date($start); $timezone = $event['DTSTART_TZID'] ?? null;
$time = (str_contains($start, 'T')) ? substr($date, 11) : 'ganztägig'; $date_info = format_ics_date_with_timezone($start, $timezone);
$date = $date_info['display'];
$time = $date_info['has_time'] ? substr($date, 11) : 'ganztägig';
$date = substr($date, 0, 10); $date = substr($date, 0, 10);
$iso_date = $date_info['iso'];
?> ?>
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50">
<td class="py-2 px-4 border-b whitespace-nowrap"><?php <td class="py-2 px-4 border-b whitespace-nowrap"><?php echo htmlspecialchars(
echo htmlspecialchars($date); ?></td> $date,
<td class="py-2 px-4 border-b whitespace-nowrap"><?php ); ?></td>
echo htmlspecialchars($time); ?></td> <td class="py-2 px-4 border-b whitespace-nowrap">
<td class="py-2 px-4 border-b"><?php <?php if ($date_info['has_time']): ?>
echo htmlspecialchars($summary); ?></td> <span class="local-time" data-iso-date="<?php echo htmlspecialchars(
$iso_date,
); ?>"><?php echo htmlspecialchars($time); ?></span>
<?php else: ?>
ganztägig
<?php endif; ?>
</td>
<td class="py-2 px-4 border-b"><?php echo htmlspecialchars($summary); ?></td>
</tr> </tr>
<?php <?php endforeach; ?>
endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php <?php endif; ?>
endif; ?>
</div> </div>
</div> </div>
<style> <style>
@@ -211,4 +205,47 @@ if ($filter_jahr && $filter_monat) {
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script>
// Zeitzonenkonvertierung für lokale Zeiten
function convertTimesToLocal() {
const timeElements = document.querySelectorAll('.local-time');
timeElements.forEach(element => {
const isoDate = element.getAttribute('data-iso-date');
if (isoDate) {
try {
// Erstelle ein Date-Objekt aus der ISO-Date
// JavaScript erkennt automatisch UTC-Zeiten (mit Z oder +00:00)
const date = new Date(isoDate);
// Prüfe ob das Datum gültig ist
if (isNaN(date.getTime())) {
console.warn('Ungültiges Datum:', isoDate);
return;
}
// Formatiere die Zeit in der lokalen Zeitzone des Browsers
const localTime = date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
// Aktualisiere den Text
element.textContent = localTime;
// Debug-Information (kann später entfernt werden)
console.log('Konvertiert:', isoDate, '->', localTime, 'in Zeitzone:', Intl.DateTimeFormat().resolvedOptions().timeZone);
} catch (error) {
console.warn('Fehler bei der Zeitzonenkonvertierung:', error, 'für Datum:', isoDate);
}
}
});
}
// Zeitzonenkonvertierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
convertTimesToLocal();
});
</script>
</section> </section>