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
+115 -39
View File
@@ -1,66 +1,142 @@
<?php
<?php
// URL der öffentlichen ICS-Datei
define(
'CAL_URL',
'https://calendar.google.com/calendar/ical/jv1bq94un3ivoa8ka0rk9ngq4k%40group.calendar.google.com/public/basic.ics',
'CAL_URL',
'https://calendar.google.com/calendar/ical/jv1bq94un3ivoa8ka0rk9ngq4k%40group.calendar.google.com/public/basic.ics',
);
function format_ics_date($date)
{
// Unterstützt sowohl ganztägige als auch Zeitangaben
if (strpos($date, 'T') !== false) {
// Unterstützt sowohl ganztägige als auch Zeitangaben
if (strpos($date, 'T') !== false) {
$dt = DateTime::createFromFormat('Ymd\THis', substr($date, 0, 15));
return $dt ? $dt->format('d.m.Y H:i') : $date;
} else {
$dt = DateTime::createFromFormat('Ymd', $date);
return $dt ? $dt->format('d.m.Y') : $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));
return $dt ? $dt->format('d.m.Y H:i') : $date;
}
} else {
$dt = DateTime::createFromFormat('Ymd', $date);
return $dt ? $dt->format('d.m.Y') : $date;
// 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 = @file_get_contents(CAL_URL);
if (! $ics) {
echo '<div class="text-red-600">Kalender konnte nicht geladen werden.</div>';
if (!$ics) {
echo '<div class="text-red-600">Kalender konnte nicht geladen werden.</div>';
return;
return;
}
// Termine parsen
function parse_ics($ics)
{
$lines = explode("\n", $ics);
$events = [];
$event = [];
$inEvent = false;
foreach ($lines as $line) {
$line = trim($line);
if ($line === 'BEGIN:VEVENT') {
$inEvent = true;
$event = [];
} elseif ($line === 'END:VEVENT') {
$inEvent = false;
$events[] = $event;
} elseif ($inEvent) {
// Property kann Parameter enthalten, z.B. DTSTART;TZID=Europe/Berlin:20240701T19000000
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$key = $parts[0];
$val = $parts[1];
// Nur den eigentlichen Property-Namen extrahieren
$key = strtoupper(preg_replace('/;.+$/', '', $key));
$event[$key] = $val;
}
}
}
$lines = explode("\n", $ics);
$events = [];
$event = [];
$inEvent = false;
foreach ($lines as $line) {
$line = trim($line);
if ($line === 'BEGIN:VEVENT') {
$inEvent = true;
$event = [];
} elseif ($line === 'END:VEVENT') {
$inEvent = false;
$events[] = $event;
} elseif ($inEvent) {
// Property kann Parameter enthalten, z.B. DTSTART;TZID=Europe/Berlin:20240701T19000000
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$key = $parts[0];
$val = $parts[1];
return $events;
// Zeitzoneninformation extrahieren
$timezone = null;
if (preg_match('/TZID=([^:;]+)/', $key, $matches)) {
$timezone = $matches[1];
}
// Nur den eigentlichen Property-Namen extrahieren
$cleanKey = strtoupper(preg_replace('/;.+$/', '', $key));
$event[$cleanKey] = $val;
// Zeitzoneninformation separat speichern
if ($timezone && in_array($cleanKey, ['DTSTART', 'DTEND'])) {
$event[$cleanKey . '_TZID'] = $timezone;
}
}
}
}
return $events;
}
$events = parse_ics($ics);
return function () use ($events) {
return $events;
return $events;
};
+90 -38
View File
@@ -1,34 +1,34 @@
<?php
$events = collection('termine');
$events = collection('termine');
// Aktuelles Datum
$today = (new DateTime())->format('Ymd');
// Aktuelles Datum
$today = new DateTime()->format('Ymd');
// Nur zukünftige Termine anzeigen
$future_events = array_filter($events, function ($event) use ($today) {
return isset($event['DTSTART']) && $event['DTSTART'] >= $today;
});
// Nur zukünftige Termine anzeigen
$future_events = array_filter($events, function ($event) use ($today) {
return isset($event['DTSTART']) && $event['DTSTART'] >= $today;
});
// Termine nach DTSTART in aufsteigender Reihenfolge sortieren
usort($future_events, function ($a, $b) {
return $a['DTSTART'] <=> $b['DTSTART'];
});
// Termine nach DTSTART in aufsteigender Reihenfolge sortieren
usort($future_events, function ($a, $b) {
return $a['DTSTART'] <=> $b['DTSTART'];
});
// --- Deutsche Monatsnamen ---
$de_months = [
'01' => 'Jan',
'02' => 'Feb',
'03' => 'Mrz',
'04' => 'Apr',
'05' => 'Mai',
'06' => 'Jun',
'07' => 'Jul',
'08' => 'Aug',
'09' => 'Sep',
'10' => 'Okt',
'11' => 'Nov',
'12' => 'Dez',
];
// --- Deutsche Monatsnamen ---
$de_months = [
'01' => 'Jan',
'02' => 'Feb',
'03' => 'Mrz',
'04' => 'Apr',
'05' => 'Mai',
'06' => 'Jun',
'07' => 'Jul',
'08' => 'Aug',
'09' => 'Sep',
'10' => 'Okt',
'11' => 'Nov',
'12' => 'Dez',
];
?>
<div class="container mx-auto py-8">
@@ -43,16 +43,19 @@
<div id="termine-container" class="flex space-x-6 pb-4 transition-transform duration-300 ease-in-out">
<?php foreach ($future_events as $event): ?>
<?php
$start = $event['DTSTART'] ?? '';
$end = $event['DTEND'] ?? '';
$summary = $event['SUMMARY'] ?? '';
$start = $event['DTSTART'] ?? '';
$end = $event['DTEND'] ?? '';
$summary = $event['SUMMARY'] ?? '';
$location = $event['LOCATION'] ?? '';
$desc = $event['DESCRIPTION'] ?? '';
$date = format_ics_date($start);
$time = (str_contains($start, 'T')) ? substr($date, 11) : 'ganztägig';
$day = substr($date, 0, 2);
$month = substr($date, 3, 2);
?>
$desc = $event['DESCRIPTION'] ?? '';
$timezone = $event['DTSTART_TZID'] ?? null;
$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);
$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">
<!-- Inhalt der ersten Karte bleibt unverändert -->
@@ -66,7 +69,9 @@
<!-- Datumsanzeige -->
<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>
<!-- Dekorative Elemente - kleine Punkte für Kalendertage -->
@@ -81,8 +86,16 @@
</div>
</div>
<div class="ml-5 flex-grow">
<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>
<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">
<?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>
@@ -169,6 +182,45 @@
// Initialer Aufruf
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
window.addEventListener('resize', () => {
// Anzahl der Karten je nach Bildschirmgröße anpassen
+134 -97
View File
@@ -4,77 +4,77 @@ $events = collection('termine');
// Nur Events mit DTSTART berücksichtigen
$events = array_filter($events, function ($event) {
return isset($event['DTSTART']) && ! empty($event['DTSTART']);
return isset($event['DTSTART']) && !empty($event['DTSTART']);
});
// Nach Datum sortieren
usort($events, function ($a, $b) {
return strcmp($a['DTSTART'], $b['DTSTART']);
return strcmp($a['DTSTART'], $b['DTSTART']);
});
// Aktuelles Datum
$today = (new DateTime())->format('Ymd');
$today = new DateTime()->format('Ymd');
// Nur zukünftige Termine anzeigen
$future_events = array_filter($events, function ($event) use ($today) {
return isset($event['DTSTART']) && $event['DTSTART'] >= $today;
return isset($event['DTSTART']) && $event['DTSTART'] >= $today;
});
// --- Deutsche Monatsnamen ---
$de_months = [
'01' => 'Januar',
'02' => 'Februar',
'03' => 'März',
'04' => 'April',
'05' => 'Mai',
'06' => 'Juni',
'07' => 'Juli',
'08' => 'August',
'09' => 'September',
'10' => 'Oktober',
'11' => 'November',
'12' => 'Dezember',
'01' => 'Januar',
'02' => 'Februar',
'03' => 'März',
'04' => 'April',
'05' => 'Mai',
'06' => 'Juni',
'07' => 'Juli',
'08' => 'August',
'09' => 'September',
'10' => 'Oktober',
'11' => 'November',
'12' => 'Dezember',
];
// --- Gruppierung aller Termine nach Jahr und Monat für die Sidebar ---
function group_events_by_year_month($events)
{
$grouped = [];
foreach ($events as $event) {
if ( ! isset($event['DTSTART'])) {
continue;
}
$date = $event['DTSTART'];
$year = substr($date, 0, 4);
$month = substr($date, 4, 2);
$grouped[$year][$month][] = $event;
}
krsort($grouped); // Jahre absteigend
foreach ($grouped as &$months) {
krsort($months); // Monate absteigend
$grouped = [];
foreach ($events as $event) {
if (!isset($event['DTSTART'])) {
continue;
}
$date = $event['DTSTART'];
$year = substr($date, 0, 4);
$month = substr($date, 4, 2);
$grouped[$year][$month][] = $event;
}
krsort($grouped); // Jahre absteigend
foreach ($grouped as &$months) {
krsort($months); // Monate absteigend
}
return $grouped;
return $grouped;
}
$all_events_grouped = group_events_by_year_month($events);
// --- Filter aus URL ---
$filter_jahr = $_GET['jahr'] ?? null;
$filter_jahr = $_GET['jahr'] ?? null;
$filter_monat = $_GET['monat'] ?? null;
if ($filter_jahr && $filter_monat) {
$filtered_events = array_filter($events, function ($event) use ($filter_jahr, $filter_monat) {
$date = $event['DTSTART'] ?? '';
$filtered_events = array_filter($events, function ($event) use ($filter_jahr, $filter_monat) {
$date = $event['DTSTART'] ?? '';
return substr($date, 0, 4) === $filter_jahr && substr($date, 4, 2) === $filter_monat;
});
return substr($date, 0, 4) === $filter_jahr && substr($date, 4, 2) === $filter_monat;
});
} elseif ($filter_jahr) {
$filtered_events = array_filter($events, function ($event) use ($filter_jahr) {
$date = $event['DTSTART'] ?? '';
$filtered_events = array_filter($events, function ($event) use ($filter_jahr) {
$date = $event['DTSTART'] ?? '';
return substr($date, 0, 4) === $filter_jahr;
});
return substr($date, 0, 4) === $filter_jahr;
});
} else {
$filtered_events = $future_events;
$filtered_events = $future_events;
}
?>
@@ -85,54 +85,43 @@ if ($filter_jahr && $filter_monat) {
<div class="bg-white rounded-lg shadow p-4">
<h2 class="text-lg font-semibold mb-3">Termine nach Jahr/Monat</h2>
<ul class="space-y-1">
<?php
foreach ($all_events_grouped as $year => $months): ?>
<?php foreach ($all_events_grouped as $year => $months): ?>
<li class="mb-2">
<div class="flex items-center">
<a href="?jahr=<?php
echo $year; ?>"
<a href="?jahr=<?php echo $year; ?>"
class="font-bold text-sf_blau-600 focus:outline-none flex items-center group<?php
if ($filter_jahr === $year && ! $filter_monat) {
echo ' underline';
} ?><?php
if ($filter_jahr === $year && ! $filter_monat) {
echo ' selected';
} ?>" onclick="event.stopPropagation(); openYear('<?php
echo $year; ?>')">
<span><?php
echo $year; ?></span>
if ($filter_jahr === $year && !$filter_monat) {
echo ' underline';
}
if ($filter_jahr === $year && !$filter_monat) {
echo ' selected';
}
?>" onclick="event.stopPropagation(); openYear('<?php echo $year; ?>')">
<span><?php echo $year; ?></span>
</a>
<button type="button" class="ml-1 focus:outline-none" onclick="toggleYear('<?php
echo $year; ?>')">
<svg class="w-4 h-4 transition-transform" id="arrow-<?php
echo $year; ?>" fill="none" stroke="currentColor" stroke-width="2"
<button type="button" class="ml-1 focus:outline-none" onclick="toggleYear('<?php echo $year; ?>')">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
<ul class="ml-4 mt-1 hidden" id="months-<?php
echo $year; ?>">
<?php
foreach ($months as $month => $evts): ?>
<ul class="ml-4 mt-1 hidden" id="months-<?php echo $year; ?>">
<?php foreach ($months as $month => $evts): ?>
<li>
<a href="?jahr=<?php
echo $year; ?>&monat=<?php
echo $month; ?>" class="text-sf_blau-500 hover:underline<?php
if ($filter_jahr === $year && $filter_monat === $month) {
echo ' font-bold underline selected';
} ?>">
<?php
echo $de_months[$month]; ?> (<?php
echo count($evts); ?>)
<a href="?jahr=<?php echo $year; ?>&monat=<?php echo $month; ?>" class="text-sf_blau-500 hover:underline<?php if (
$filter_jahr === $year &&
$filter_monat === $month
) {
echo ' font-bold underline selected';
} ?>">
<?php echo $de_months[$month]; ?> (<?php echo count($evts); ?>)
</a>
</li>
<?php
endforeach; ?>
<?php endforeach; ?>
</ul>
</li>
<?php
endforeach; ?>
<?php endforeach; ?>
</ul>
</div>
<script>
@@ -160,11 +149,9 @@ if ($filter_jahr && $filter_monat) {
</aside>
<!-- Hauptinhalt Termine -->
<div class="md:w-3/4 w-full">
<?php
if (empty($filtered_events)): ?>
<?php if (empty($filtered_events)): ?>
<div class="text-gray-500">Keine Termine gefunden.</div>
<?php
else: ?>
<?php else: ?>
<div class="overflow-x-auto">
<table class="min-w-full border border-gray-200 bg-white rounded-lg shadow">
<thead>
@@ -175,33 +162,40 @@ if ($filter_jahr && $filter_monat) {
</tr>
</thead>
<tbody>
<?php
foreach ($filtered_events as $event): ?>
<?php foreach ($filtered_events as $event): ?>
<?php
$start = $event['DTSTART'] ?? '';
$end = $event['DTEND'] ?? '';
$summary = $event['SUMMARY'] ?? '';
$start = $event['DTSTART'] ?? '';
$end = $event['DTEND'] ?? '';
$summary = $event['SUMMARY'] ?? '';
$location = $event['LOCATION'] ?? '';
$desc = $event['DESCRIPTION'] ?? '';
$date = format_ics_date($start);
$time = (str_contains($start, 'T')) ? substr($date, 11) : 'ganztägig';
$date = substr($date, 0, 10);
$desc = $event['DESCRIPTION'] ?? '';
$timezone = $event['DTSTART_TZID'] ?? null;
$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);
$iso_date = $date_info['iso'];
?>
<tr class="hover:bg-gray-50">
<td class="py-2 px-4 border-b whitespace-nowrap"><?php
echo htmlspecialchars($date); ?></td>
<td class="py-2 px-4 border-b whitespace-nowrap"><?php
echo htmlspecialchars($time); ?></td>
<td class="py-2 px-4 border-b"><?php
echo htmlspecialchars($summary); ?></td>
<td class="py-2 px-4 border-b whitespace-nowrap"><?php echo htmlspecialchars(
$date,
); ?></td>
<td class="py-2 px-4 border-b whitespace-nowrap">
<?php if ($date_info['has_time']): ?>
<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>
<?php
endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
endif; ?>
<?php endif; ?>
</div>
</div>
<style>
@@ -211,4 +205,47 @@ if ($filter_jahr && $filter_monat) {
font-weight: bold;
}
</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>