Quelltext Kalenderübersicht
Version 08.06.25 -
spielkalenderevents.php
<?php
ini_set('display_errors', 1); // nichts mehr anzeigen
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
date_default_timezone_set('Europe/Berlin');
/* --------- Mini-Autoloader (passt auf deine Struktur) ---------- */
spl_autoload_register(function ($class) {
$prefix = 'Rw4lll\\ICal\\'; // exakter Namespace im Paket
$base = __DIR__ . '/spielkalenderlib/'; // dort liegen deine Dateien
$len = strlen($prefix);
// gehört die Klasse zum Paket?
if (strncmp($class, $prefix, $len) !== 0) {
return; // anderes Paket – nichts tun
}
// Rest in Dateiname umsetzen, z. B. Rw4lll\ICal\TimeZoneMaps -> TimeZoneMaps.php
$relative = substr($class, $len);
$file = $base . $relative . '.php';
if (file_exists($file)) {
require $file; // ← lädt ICal.php, TimeZoneMaps.php, …
}
});
/* --------------------------------------------------------------- */
use Rw4lll\ICal\ICal; // ab hier findet PHP die Klasse
// ------------------------------------------------------------------
// 1) ICS-Feed holen (5-Min-Cache)
$icsUrl = 'https://calendar.google.com/calendar/ical/'
. 'e3d5d97289d5e00768c1d70b1db34f8bb13f4696947cb76b1f725e903d1e684f@group.calendar.google.com/public/basic.ics';
$cacheFile = sys_get_temp_dir() . '/calendar_basic.ics';
$cacheTtl = 300;
if (!is_file($cacheFile) || filemtime($cacheFile) < time() - $cacheTtl) {
$ctx = stream_context_create(['http' => ['timeout' => 10]]);
$data = @file_get_contents($icsUrl, false, $ctx); // @ = Warnungen unterdrücken
if ($data !== false && strlen($data) > 0) {
file_put_contents($cacheFile, $data);
}
}
// ------------------------------------------------------------------
/* 2) ICS parsen -------------------------------------------------- */
$options = [
'defaultSpan' => 1,
'defaultTimeZone' => 'Europe/Berlin',
/* Serien auflösen */
'skipRecurrence' => false,
/* ▶ Fenster anpassen ◀ */
'filterDaysBefore' => 1, // 1 Tag zurück → heute komplett dabei
'filterDaysAfter' => 365, // unverändert
];
$lines = file($cacheFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$ical = new Rw4lll\ICal\ICal($lines, $options);
/* hier ändern ↓ */
$events = $ical->getEvents(); // statt $ical->events;
// ------------------------------------------------------------------
// 3) Filter einlesen
$weekday = $_GET['weekday'] ?? ''; // '0' = So … '6' = Sa
$q = strtolower($_GET['q'] ?? '');
$place = strtolower($_GET['place'] ?? '');
$after = $_GET['after'] ?? ''; // ISO-Datum (yyyy-mm-dd)
if (!function_exists('str_contains')) { // Kompatibilität < PHP 8
function str_contains($haystack, $needle) {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
// ------------------------------------------------------------------
// 4) Filtern
$filtered = array_filter($events, function ($ev) use ($weekday, $q, $place, $after) {
// Zeitpunkt (ganztägig vs. mit Uhrzeit)
$tsStart = $ev->dtstart_array[2] ?? strtotime($ev->dtstart);
if ($weekday !== '' && date('w', $tsStart) != $weekday) return false;
if ($after !== '' && $tsStart < strtotime($after)) return false;
$text = strtolower(($ev->summary ?? '') . ($ev->description ?? ''));
$loc = strtolower($ev->location ?? '');
return str_contains($text, $q) && str_contains($loc, $place);
});
// ------------------------------------------------------------------
// 5) Schlankes Array bauen
// ===== Hilfsfunktion: DateTime|ICS-String → ISO-8601 =============
function icsToIso($value): string
{
/* Fall 1: Bibliothek gibt bereits DateTime-Objekt zurück */
if ($value instanceof DateTimeInterface) {
return $value->format('Y-m-d\TH:i:s'); // z. B. 2025-06-12T18:00:00
}
/* Fall 2: Roh-String wie 20250612T180000Z oder 20250612T180000 */
if (is_string($value) &&
preg_match('/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/', $value, $m)) {
return "$m[1]-$m[2]-$m[3]T$m[4]:$m[5]:$m[6]"; // ebenfalls ohne Offset
}
/* Unbekanntes Format → unverändert zurückgeben */
return (string)$value;
}
// =================================================================
/* ===== Hilfsfunktion: ICS-Escapes entfernen ==================== */
function icsUnescape(string $txt): string
{
$txt = str_replace(['\\,', '\\;'], [',', ';'], $txt); // \, \;
$txt = preg_replace('/\\\\n/i', "\n", $txt); // \n oder \N
$txt = str_replace('\\\\', '\\', $txt); // \\ → \
return trim($txt);
}
/* -------- 5) Schlankes Array bauen ----------------------------- */
$out = array_map(function ($ev) {
return [
'title' => icsUnescape($ev->summary),
'start' => icsToIso($ev->dtstart),
'end' => icsToIso($ev->dtend),
'where' => icsUnescape($ev->location),
'desc' => icsUnescape($ev->description),
'url' => $ev->url ?? '',
];
}, $filtered);
// ------------------------------------------------------------------
// 6) JSON ausgeben (NEU: Fehler-Check)
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
$json = json_encode(array_values($out), JSON_UNESCAPED_UNICODE);
if ($json === false) { // <- schlug fehl?
http_response_code(500);
echo json_encode([
'error' => 'json_encode',
'msg' => json_last_error_msg() // z. B. "Malformed UTF-8 characters"
]);
exit;
}
echo $json;
Quelltext spielkalender.html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Kalender-Filter</title>
<style>
body{font-family:sans-serif;margin:2rem}
label{display:inline-block;width:6rem}
input,select{margin:0 .5rem 1rem 0}
#events tbody tr:nth-child(even){background:#f7f7f7}
th{cursor:pointer}
</style>
</head>
<body>
<h2>Termine filtern</h2>
<label>Zeitraum:</label>
<select id="range">
<option value="all">alle</option>
<option value="7">7 Tage</option>
<option value="14">14 Tage</option>
<option value="month">dieser Monat</option>
</select>
<label>Tag:</label>
<select id="weekday">
<option value="">alle</option>
<option value="1">Mo</option><option value="2">Di</option>
<option value="3">Mi</option><option value="4">Do</option>
<option value="5">Fr</option><option value="6">Sa</option>
<option value="0">So</option>
</select>
<label>Ort:</label><input id="place" placeholder="z.B. Berlin">
<label>Suche:</label><input id="search" placeholder="Stichwort">
<table id="events">
<thead>
<tr>
<th data-key="title">Titel ▲▼</th>
<th data-key="where">Ort ▲▼</th>
<th data-key="start">Start ▲▼</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script>
const $ = s => document.querySelector(s);
const state = { weekday:'', place:'', q:'', range:'all', sortKey:'start', asc:true };
let allEvents = [];
/* ---------- URL ⇄ State ---------- */
function readURLtoState(){
const p = new URLSearchParams(location.search);
state.range = p.get('range') || 'all';
state.weekday = p.get('day') || '';
state.place = p.get('place') || '';
state.q = p.get('q') || '';
$('#range').value = state.range;
$('#weekday').value = state.weekday;
$('#place').value = state.place;
$('#search').value = state.q;
}
function writeStateToURL(){
const p = new URLSearchParams();
if (state.range && state.range !== 'all') p.set('range', state.range);
if (state.weekday) p.set('day', state.weekday);
if (state.place) p.set('place', state.place);
if (state.q) p.set('q', state.q);
history.replaceState(null,'',`${encodeURI(location.pathname)}?${p.toString()}`);
}
/* ---------- Filter-Controls ---------- */
$('#weekday').onchange = e => { state.weekday = e.target.value; writeStateToURL(); load(); };
$('#place').oninput = e => { state.place = e.target.value; writeStateToURL(); load(); };
$('#search').oninput = e => { state.q = e.target.value; writeStateToURL(); load(); };
$('#range').onchange = e => { state.range = e.target.value; writeStateToURL(); load(); };
/* ---------- Sortier-Klick ---------- */
document.querySelectorAll('#events th').forEach(th=>{
th.onclick = ()=>{
const k = th.dataset.key;
state.asc = (state.sortKey===k)? !state.asc : true;
state.sortKey = k;
writeStateToURL();
load();
};
});
/* ---------- Daten einmalig laden ---------- */
async function fetchEvents(){
if (allEvents.length) return;
const res = await fetch('spielkalenderevents.php');
allEvents = await res.json();
}
/* ---------- Zeitraum-Check ---------- */
function inRange(ts){
const now = Date.now();
switch(state.range){
case '7': return ts <= now + 7*864e5;
case '14': return ts <= now +14*864e5;
case 'month': {
const d = new Date();
const end = new Date(d.getFullYear(), d.getMonth()+1, 1);
return ts < end;
}
default: return true;
}
}
/* ---------- Hauptfunktion ---------- */
async function load(){
await fetchEvents();
let rows = allEvents.filter(ev=>{
const ts = Date.parse(ev.start);
if (!inRange(ts)) return false;
if (state.weekday && new Date(ts).getDay() != state.weekday) return false;
if (state.place && !(ev.where||'').toLowerCase().includes(state.place.toLowerCase())) return false;
if (state.q && !(ev.title+ev.desc).toLowerCase().includes(state.q.toLowerCase())) return false;
return true;
});
rows.sort((a,b)=>{
const ak = a[state.sortKey] || '', bk = b[state.sortKey] || '';
if (state.sortKey === 'start') return (new Date(ak)-new Date(bk))*(state.asc?1:-1);
return ak.localeCompare(bk,'de-DE')*(state.asc?1:-1);
});
document.querySelector('#events tbody').innerHTML = rows.map(ev=>`
<tr>
<td>${ev.title}</td>
<td>${ev.where||''}</td>
<td>${new Date(ev.start).toLocaleString('de-DE')}</td>
</tr>`).join('');
}
/* Init */
readURLtoState();
load();
</script>
</body>
</html>