Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-26 14:36:24 +02:00
parent 242bfac96b
commit 75b5944d12

View File

@@ -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;