From 75b5944d12506ee42404b4fb4801072a054a63c9 Mon Sep 17 00:00:00 2001 From: Dicken Date: Tue, 26 May 2026 14:36:24 +0200 Subject: [PATCH] Dateien nach "frontend/src" hochladen --- frontend/src/calendar.jsx | 84 ++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/frontend/src/calendar.jsx b/frontend/src/calendar.jsx index 9ed476e..d77b041 100644 --- a/frontend/src/calendar.jsx +++ b/frontend/src/calendar.jsx @@ -2,38 +2,66 @@ import { useState, useEffect, useMemo } from 'react'; import { api, S } from './lib.js'; -// Minimaler iCal-Parser (VEVENT Felder) -function parseIcal(text) { +// iCal-Parser – unterstützt gefaltete Zeilen, TZID, VALUE=DATE +function parseIcal(raw) { + // 1. Gefaltete Zeilen (RFC 5545) zusammenführen + const text = raw.replace(/\r\n[ \t]/g, '').replace(/\r/g, ''); + + // 2. Alle Properties als Map extrahieren + function getProps(block) { + const props = {}; + for (const line of block.split('\n')) { + const col = line.indexOf(':'); + if (col < 0) continue; + const namepart = line.slice(0, col).toUpperCase(); + const val = line.slice(col + 1).trim(); + // Basisname ohne Parameter + const name = namepart.split(';')[0]; + // TZID-Parameter extrahieren + const tzidM = namepart.match(/TZID=([^;:]+)/); + props[name] = { val, raw: namepart, tzid: tzidM?.[1] || null }; + } + return props; + } + + // 3. Datum parsen – Ortszeit wenn TZID gesetzt, UTC wenn Z-Suffix + function parseDate(prop, isEnd) { + if (!prop) return null; + const s = prop.val; + if (!s) return null; + const allDay = s.length === 8 || prop.raw.includes('VALUE=DATE'); + if (allDay) { + // DTEND bei all-day ist exklusiv → einen Tag abziehen + const d = new Date(+s.slice(0,4), +s.slice(4,6)-1, +s.slice(6,8)); + if (isEnd) d.setDate(d.getDate() - 1); + return { date: d, allDay: true }; + } + const utc = s.endsWith('Z'); + const y=+s.slice(0,4), mo=+s.slice(4,6)-1, d=+s.slice(6,8); + const h=+s.slice(9,11)||0, mi=+s.slice(11,13)||0, sec=+s.slice(13,15)||0; + const date = utc + ? new Date(Date.UTC(y,mo,d,h,mi,sec)) + : new Date(y,mo,d,h,mi,sec); // Ortszeit des Browsers (beste Annäherung) + return { date, allDay: false }; + } + const events = []; const blocks = text.split('BEGIN:VEVENT'); for (let i = 1; i < blocks.length; i++) { - const b = blocks[i]; - const get = (key) => { - const m = b.match(new RegExp(`${key}(?:;[^:]*)?:([^\r\n]+)`, 'i')); - return m ? m[1].trim() : ''; - }; - const parseDate = (s) => { - if (!s) return null; - // Remove VALUE=DATE: prefix handling - const clean = s.replace(/^[^:]*:/,''); - if (clean.length === 8) { - // all-day: YYYYMMDD - return new Date(+clean.slice(0,4), +clean.slice(4,6)-1, +clean.slice(6,8)); - } - // datetime: YYYYMMDDTHHMMSSZ - return new Date( - Date.UTC(+clean.slice(0,4), +clean.slice(4,6)-1, +clean.slice(6,8), - +clean.slice(9,11)||0, +clean.slice(11,13)||0, +clean.slice(13,15)||0) - ); - }; - const dtstart = parseDate(get('DTSTART')); - const dtend = parseDate(get('DTEND')) || dtstart; - if (!dtstart || isNaN(dtstart)) continue; + const end = blocks[i].indexOf('END:VEVENT'); + const block = end >= 0 ? blocks[i].slice(0, end) : blocks[i]; + const p = getProps(block); + + const startObj = parseDate(p['DTSTART']); + const endObj = parseDate(p['DTEND'], true) || startObj; + if (!startObj || isNaN(startObj.date)) continue; + events.push({ - summary: get('SUMMARY') || '(kein Titel)', - location: get('LOCATION') || '', - dtstart, dtend, - allDay: (get('DTSTART') || '').replace(/^[^:]*:/,'').length === 8, + summary: (p['SUMMARY']?.val || '(kein Titel)').replace(/\\n/g,'\n').replace(/\\,/g,','), + location: (p['LOCATION']?.val || '').replace(/\\n/g,'\n').replace(/\\,/g,','), + dtstart: startObj.date, + dtend: endObj.date, + allDay: startObj.allDay, }); } return events;