Enhances event handling with timezone support
Adds timezone handling to the event parsing and formatting logic. This allows events to be displayed in the user's local timezone, improving the user experience. Also restructures the event display on the "Termine" page to include a sidebar for filtering by year and month.
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
Reference in New Issue
Block a user