Quelltext Kalenderübersicht

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>