feat: add termine.php template and integration with ICS calendar feed for event fetching, parsing, and display

This commit is contained in:
2025-06-28 18:37:13 +02:00
parent b259808cd2
commit 3346d16c3f
4 changed files with 177 additions and 1 deletions
+26
View File
@@ -8,6 +8,7 @@
monospace;
--color-red-50: oklch(97.1% 0.013 17.38);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-pink-500: oklch(65.6% 0.241 354.308);
@@ -21,6 +22,7 @@
--color-white: #fff;
--spacing: 0.25rem;
--container-lg: 32rem;
--container-3xl: 48rem;
--container-7xl: 80rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
@@ -362,6 +364,9 @@
.w-full {
width: 100%;
}
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-7xl {
max-width: var(--container-7xl);
}
@@ -473,6 +478,10 @@
border-top-style: var(--tw-border-style);
border-top-width: 1px;
}
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-gray-200 {
border-color: var(--color-gray-200);
}
@@ -512,6 +521,9 @@
.object-cover {
object-fit: cover;
}
.p-4 {
padding: calc(var(--spacing) * 4);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
@@ -640,6 +652,9 @@
.text-red-500 {
color: var(--color-red-500);
}
.text-red-600 {
color: var(--color-red-600);
}
.text-sf_blau-500 {
color: var(--color-sf_blau-500);
}
@@ -674,6 +689,10 @@
.opacity-50 {
opacity: 50%;
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -766,6 +785,13 @@
}
}
}
.hover\:bg-gray-50 {
&:hover {
@media (hover: hover) {
background-color: var(--color-gray-50);
}
}
}
.hover\:bg-gray-100 {
&:hover {
@media (hover: hover) {
+4
View File
@@ -1,3 +1,7 @@
Title: Termine
----
HeroText: Wir möchten Sie gerne über die bevorstehenden Termine in unserem Verein informieren. Halten Sie sich bereit für spannende Ereignisse und Veranstaltungen, die bald anstehen. Details zu den Terminen und Veranstaltungen finden Sie auf unserer Homepage.
----
+112
View File
@@ -0,0 +1,112 @@
<?php
// URL der öffentlichen ICS-Datei
define('CAL_URL', 'https://calendar.google.com/calendar/ical/jv1bq94un3ivoa8ka0rk9ngq4k%40group.calendar.google.com/public/basic.ics');
// ICS-Datei laden
$ics = @file_get_contents(CAL_URL);
if (!$ics) {
echo '<div class="text-red-600">Kalender konnte nicht geladen werden.</div>';
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;
}
}
}
return $events;
}
function format_ics_date($date) {
// 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;
}
}
$events = parse_ics($ics);
// Nur Events mit DTSTART berücksichtigen
$events = array_filter($events, function($event) {
return isset($event['DTSTART']) && !empty($event['DTSTART']);
});
// Nach Datum sortieren
usort($events, function($a, $b) {
return strcmp($a['DTSTART'], $b['DTSTART']);
});
// 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;
});
?>
<section class="py-24 bg-sf_grau-50">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<?php if (empty($future_events)): ?>
<div class="text-gray-500">Keine bevorstehenden Termine gefunden.</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="min-w-full border border-gray-200 bg-white rounded-lg shadow">
<thead>
<tr class="bg-gray-100">
<th class="py-2 px-4 border-b text-left">Datum</th>
<th class="py-2 px-4 border-b text-left">Uhrzeit</th>
<th class="py-2 px-4 border-b text-left">Titel</th>
<th class="py-2 px-4 border-b text-left">Ort</th>
<th class="py-2 px-4 border-b text-left">Beschreibung</th>
</tr>
</thead>
<tbody>
<?php foreach ($future_events as $event): ?>
<?php
$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);
?>
<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"><?php echo htmlspecialchars($location); ?></td>
<td class="py-2 px-4 border-b"><?php echo nl2br(htmlspecialchars($desc)); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</section>
+34
View File
@@ -0,0 +1,34 @@
<?php
/*
Templates render the content of your pages.
They contain the markup together with some control structures
like loops or if-statements. The `$page` variable always
refers to the currently active page.
To fetch the content from each field we call the field name as a
method on the `$page` object, e.g. `$page->title()`.
This default template must not be removed. It is used whenever Kirby
cannot find a template with the name of the content file.
Snippets like the header and footer contain markup used in
multiple templates. They also help to keep templates clean.
More about templates: https://getkirby.com/docs/guide/templates/basics
*/
?>
<?php snippet('header') ?>
<body>
<?php snippet('navbar') ?>
<?php snippet('titel') ?>
<?php snippet('termine') ?>
<?= js('assets/js/navbar.js') ?>
</body>
<?php snippet('footer') ?>