feat: introduce event carousel and ICS integration for homepage
Added an event carousel to the homepage to display upcoming events dynamically. Integrated ICS parsing to fetch and format calendar data for future events. Enhanced layout and responsiveness with scroll functionality and improved styling.
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
// URL der öffentlichen ICS-Datei
|
||||||
|
define(
|
||||||
|
'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) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = parse_ics($ics);
|
||||||
|
|
||||||
|
return function() use ($events) {
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
$events = collection('termine');
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 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">
|
||||||
|
<h2>Bevorstehende Termine</h2>
|
||||||
|
|
||||||
|
<div class="relative flex items-center justify-center">
|
||||||
|
<button id="scroll-left" class="flex-shrink-0 bg-white p-3 rounded-full shadow-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 z-10 hidden md:block mr-4">
|
||||||
|
<svg class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="termine-wrapper" class="relative w-full max-w-7xl overflow-hidden">
|
||||||
|
<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'] ?? '';
|
||||||
|
$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);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="termine-card flex-none w-full md:w-1/3 bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl shadow-xl transform hover:scale-102 transition-all duration-300 ease-in-out border border-blue-200">
|
||||||
|
<!-- Inhalt der ersten Karte bleibt unverändert -->
|
||||||
|
<div class="p-6 flex flex-col h-full">
|
||||||
|
<div class="flex items-center mb-5">
|
||||||
|
<div class="relative flex-shrink-0 bg-white rounded-lg shadow-md overflow-hidden w-16 h-16 flex flex-col border border-blue-200">
|
||||||
|
<!-- Kopfzeile des Kalenders mit Monat -->
|
||||||
|
<div class="bg-blue-600 text-white text-xs font-semibold py-1 text-center uppercase">
|
||||||
|
<span data-month><?php echo $de_months[$month]; ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Dekorative Elemente - kleine Punkte für Kalendertage -->
|
||||||
|
<div class="absolute bottom-1 left-0 right-0 flex justify-center space-x-0.5 px-1">
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-200"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-300"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-400"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-500"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-400"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-300"></div>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-sf_blau-200"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 flex-grow">
|
||||||
|
<h3 class="text-xl font-bold text-sf_grau-900"><?= $event["SUMMARY"] ?></h3>
|
||||||
|
<p class="text-sf_blau-700 text-lg font-medium mt-1">ab <?php echo htmlspecialchars($time); ?> Uhr</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="scroll-right" class="flex-shrink-0 bg-white p-3 rounded-full shadow-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 z-10 hidden md:block ml-4">
|
||||||
|
<svg class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const termineWrapper = document.getElementById('termine-wrapper');
|
||||||
|
const termineContainer = document.getElementById('termine-container');
|
||||||
|
const scrollLeftBtn = document.getElementById('scroll-left');
|
||||||
|
const scrollRightBtn = document.getElementById('scroll-right');
|
||||||
|
const cards = document.querySelectorAll('.termine-card');
|
||||||
|
|
||||||
|
let currentPosition = 0;
|
||||||
|
let totalCards = cards.length;
|
||||||
|
let cardsToShow = window.innerWidth < 768 ? 1 : 3; // Nur 1 Karte auf mobilen Geräten
|
||||||
|
let maxPosition = Math.max(0, totalCards - cardsToShow);
|
||||||
|
|
||||||
|
// Funktion zum Aktualisieren der Sichtbarkeit der Pfeile
|
||||||
|
function updateArrowVisibility() {
|
||||||
|
scrollLeftBtn.classList.toggle('opacity-50', currentPosition <= 0);
|
||||||
|
scrollRightBtn.classList.toggle('opacity-50', currentPosition >= maxPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisierung - verstecke die Pfeile wenn weniger als cardsToShow+1 Karten vorhanden sind
|
||||||
|
if (totalCards <= cardsToShow) {
|
||||||
|
scrollLeftBtn.classList.add('hidden');
|
||||||
|
scrollRightBtn.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
scrollLeftBtn.classList.remove('hidden');
|
||||||
|
scrollRightBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Scrollen nach links
|
||||||
|
scrollLeftBtn.addEventListener('click', () => {
|
||||||
|
if (currentPosition > 0) {
|
||||||
|
currentPosition--;
|
||||||
|
scrollToPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Scrollen nach rechts
|
||||||
|
scrollRightBtn.addEventListener('click', () => {
|
||||||
|
if (currentPosition < maxPosition) {
|
||||||
|
currentPosition++;
|
||||||
|
scrollToPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Scrollen zur aktuellen Position
|
||||||
|
function scrollToPosition() {
|
||||||
|
const cardWidth = cards[0].offsetWidth;
|
||||||
|
const gapWidth = 24; // entspricht space-x-6 (1.5rem = 24px)
|
||||||
|
const scrollAmount = currentPosition * (cardWidth + gapWidth);
|
||||||
|
|
||||||
|
termineContainer.style.transform = `translateX(-${scrollAmount}px)`;
|
||||||
|
updateArrowVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialer Aufruf
|
||||||
|
updateArrowVisibility();
|
||||||
|
|
||||||
|
// Responsives Verhalten - bei Fenstergröße-Änderung
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
// Anzahl der Karten je nach Bildschirmgröße anpassen
|
||||||
|
cardsToShow = window.innerWidth < 768 ? 1 : 3;
|
||||||
|
maxPosition = Math.max(0, totalCards - cardsToShow);
|
||||||
|
|
||||||
|
// Sicherstellen, dass die aktuelle Position gültig ist
|
||||||
|
currentPosition = Math.min(currentPosition, maxPosition);
|
||||||
|
|
||||||
|
// Position aktualisieren
|
||||||
|
scrollToPosition();
|
||||||
|
|
||||||
|
// Pfeile aktualisieren
|
||||||
|
if (totalCards <= cardsToShow) {
|
||||||
|
scrollLeftBtn.classList.add('hidden');
|
||||||
|
scrollRightBtn.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
scrollLeftBtn.classList.remove('hidden');
|
||||||
|
scrollRightBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Versteckt die Scrollbar vollständig, behält aber die Scrollfunktionalität */
|
||||||
|
#termine-container {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#termine-container::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gleiche Kartenbreiten sicherstellen */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.termine-card {
|
||||||
|
width: calc((100% - 48px) / 3); /* 100% minus 2 Gaps (2 * 24px) geteilt durch 3 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auf mobilen Geräten eine Karte anzeigen */
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.termine-card {
|
||||||
|
width: 100%; /* Volle Breite */
|
||||||
|
}
|
||||||
|
#termine-container {
|
||||||
|
space-x-0; /* Kein Abstand zwischen Karten nötig */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stellen Sie sicher, dass Texte abgeschnitten werden, wenn sie zu lang sind */
|
||||||
|
.termine-card h3 {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* white-space: nowrap; */ /* Diese Zeile entfernen oder kommentieren */
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2; /* Maximal 2 Zeilen anzeigen */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,50 +1,5 @@
|
|||||||
<?php
|
<?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)
|
function format_ics_date($date)
|
||||||
{
|
{
|
||||||
// Unterstützt sowohl ganztägige als auch Zeitangaben
|
// Unterstützt sowohl ganztägige als auch Zeitangaben
|
||||||
@@ -59,7 +14,8 @@ function format_ics_date($date)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$events = parse_ics($ics);
|
$events = collection('termine');
|
||||||
|
|
||||||
// Nur Events mit DTSTART berücksichtigen
|
// Nur Events mit DTSTART berücksichtigen
|
||||||
$events = array_filter($events, function ($event) {
|
$events = array_filter($events, function ($event) {
|
||||||
return isset($event['DTSTART']) && ! empty($event['DTSTART']);
|
return isset($event['DTSTART']) && ! empty($event['DTSTART']);
|
||||||
@@ -135,6 +91,7 @@ if ($filter_jahr && $filter_monat) {
|
|||||||
$filtered_events = $future_events;
|
$filtered_events = $future_events;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<section class="py-24 bg-sf_grau-50">
|
<section class="py-24 bg-sf_grau-50">
|
||||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row gap-8">
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row gap-8">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php snippet('layout', slots: true) ?>
|
||||||
|
|
||||||
|
<h1>Hallo Welt!</h1>
|
||||||
|
|
||||||
|
<?php snippet('termine-home') ?>
|
||||||
|
|
||||||
|
<?php endsnippet() ?>
|
||||||
Reference in New Issue
Block a user