321 lines
14 KiB
PHP
321 lines
14 KiB
PHP
<?php
|
|
$events = collection('termine');
|
|
|
|
if ($events === null): ?>
|
|
<div class="container mx-auto py-8">
|
|
<h2>Bevorstehende Termine</h2>
|
|
<div class="text-red-600">Die Termine können gerade nicht geladen werden.</div>
|
|
</div>
|
|
<?php return; endif;
|
|
|
|
// Aktuelles Datum - PHP 8.3 kompatibel
|
|
$today = (new DateTime('now'))->format('Ymd');
|
|
|
|
// 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'];
|
|
});
|
|
|
|
// --- 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'] ?? '';
|
|
$timezone = $event['DTSTART_TZID'] ?? null;
|
|
|
|
// Sichere Funktionsaufrufe mit Null-Checks
|
|
$date_info = function_exists('format_ics_date_with_timezone')
|
|
? format_ics_date_with_timezone($start, $timezone)
|
|
: ['display' => '', 'has_time' => false, 'iso' => ''];
|
|
|
|
$date = $date_info['display'] ?? '';
|
|
$time = ($date_info['has_time'] ?? false) ? substr($date, 11) : 'ganztägig';
|
|
$day = substr($date, 0, 2);
|
|
$month = substr($date, 3, 2);
|
|
$iso_date = $date_info['iso'] ?? '';
|
|
|
|
// Sichere Array-Zugriffe
|
|
$month_name = $de_months[$month] ?? 'Unbek';
|
|
$event_summary = htmlspecialchars($summary, ENT_QUOTES, 'UTF-8');
|
|
$safe_day = htmlspecialchars($day, ENT_QUOTES, 'UTF-8');
|
|
$safe_iso_date = htmlspecialchars($iso_date, ENT_QUOTES, 'UTF-8');
|
|
$safe_time = htmlspecialchars($time, ENT_QUOTES, 'UTF-8');
|
|
?>
|
|
|
|
<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 -->
|
|
<div class="p-6 flex flex-col h-full">
|
|
<div class="flex items-top 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><?= $month_name ?></span>
|
|
</div>
|
|
|
|
<!-- Datumsanzeige -->
|
|
<div class="flex-grow flex items-center justify-center">
|
|
<div class="text-3xl font-bold text-sf_grau-800" data-day><?= $safe_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">
|
|
<p 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'] ?? false): ?>
|
|
ab <span class="local-time"
|
|
data-iso-date="<?= $safe_iso_date ?>"><?= $safe_time ?></span> Uhr
|
|
<?php else: ?>
|
|
ganztägig
|
|
<?php endif; ?>
|
|
</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 des Zustands der Pfeile
|
|
function updateArrowState() {
|
|
// Linker Pfeil deaktivieren, wenn es keine vorherigen Termine gibt
|
|
if (currentPosition <= 0) {
|
|
scrollLeftBtn.disabled = true;
|
|
scrollLeftBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
scrollLeftBtn.classList.remove('hover:bg-gray-100');
|
|
} else {
|
|
scrollLeftBtn.disabled = false;
|
|
scrollLeftBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
scrollLeftBtn.classList.add('hover:bg-gray-100');
|
|
}
|
|
|
|
// Rechter Pfeil deaktivieren, wenn es keine weiteren Termine gibt
|
|
if (currentPosition >= maxPosition) {
|
|
scrollRightBtn.disabled = true;
|
|
scrollRightBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
scrollRightBtn.classList.remove('hover:bg-gray-100');
|
|
} else {
|
|
scrollRightBtn.disabled = false;
|
|
scrollRightBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
scrollRightBtn.classList.add('hover:bg-gray-100');
|
|
}
|
|
}
|
|
|
|
// Initialisierung - Pfeile immer anzeigen, aber entsprechend deaktivieren
|
|
// Entferne alle hidden-Klassen, damit Buttons immer sichtbar sind
|
|
scrollLeftBtn.classList.remove('hidden');
|
|
scrollRightBtn.classList.remove('hidden');
|
|
|
|
// Funktion zum Scrollen nach links
|
|
scrollLeftBtn.addEventListener('click', () => {
|
|
if (currentPosition > 0 && !scrollLeftBtn.disabled) {
|
|
currentPosition--;
|
|
scrollToPosition();
|
|
}
|
|
});
|
|
|
|
// Funktion zum Scrollen nach rechts
|
|
scrollRightBtn.addEventListener('click', () => {
|
|
if (currentPosition < maxPosition && !scrollRightBtn.disabled) {
|
|
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)`;
|
|
updateArrowState();
|
|
}
|
|
|
|
// 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
|
|
cardsToShow = window.innerWidth < 768 ? 1 : 1;
|
|
maxPosition = Math.max(0, totalCards - cardsToShow);
|
|
|
|
// Sicherstellen, dass die aktuelle Position gültig ist
|
|
currentPosition = Math.min(currentPosition, maxPosition);
|
|
|
|
// Position aktualisieren
|
|
scrollToPosition();
|
|
|
|
// Pfeile-Zustand aktualisieren
|
|
updateArrowState();
|
|
});
|
|
});
|
|
</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: 100%;
|
|
/* 100% minus 2 Gaps (2 * 24px) geteilt durch 3 */
|
|
}
|
|
|
|
#termine-container {
|
|
space-x-0;
|
|
/* Kein Abstand zwischen Karten nötig */
|
|
}
|
|
}
|
|
|
|
/* 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>
|