From 061c8bad3bde0b2e0bc1a020e4a7b61f29bb2afa Mon Sep 17 00:00:00 2001
From: Dicken
Date: Tue, 26 May 2026 14:29:05 +0200
Subject: [PATCH] Dateien nach "frontend/src" hochladen
---
frontend/src/App.jsx | 5 +
frontend/src/calendar.jsx | 285 ++++++++++++++++++++++++++++++++++++++
2 files changed, 290 insertions(+)
create mode 100644 frontend/src/calendar.jsx
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index eae5a44..7ca2049 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { api, S } from './lib.js';
import { TOOLS, getGroupedTools } from './toolRegistry.js';
+import CalendarWidget, { CalendarSettings } from './calendar.jsx';
import { useConfirm } from './confirm.jsx';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon, TrashIcon } from './icons.jsx';
@@ -704,6 +705,7 @@ function Dashboard({ toast, mobile }) {
Löscht deinen Account und alle deine Daten unwiderruflich.
diff --git a/frontend/src/calendar.jsx b/frontend/src/calendar.jsx
new file mode 100644
index 0000000..9ed476e
--- /dev/null
+++ b/frontend/src/calendar.jsx
@@ -0,0 +1,285 @@
+// ── Kalender Widget mit iCal-Unterstützung ────────────────────────────────────
+import { useState, useEffect, useMemo } from 'react';
+import { api, S } from './lib.js';
+
+// Minimaler iCal-Parser (VEVENT Felder)
+function parseIcal(text) {
+ 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;
+ events.push({
+ summary: get('SUMMARY') || '(kein Titel)',
+ location: get('LOCATION') || '',
+ dtstart, dtend,
+ allDay: (get('DTSTART') || '').replace(/^[^:]*:/,'').length === 8,
+ });
+ }
+ return events;
+}
+
+function sameDay(a, b) {
+ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate();
+}
+
+function inRange(event, day) {
+ const d = new Date(day); d.setHours(0,0,0,0);
+ const s = new Date(event.dtstart); s.setHours(0,0,0,0);
+ const e = new Date(event.dtend); e.setHours(0,0,0,0);
+ return d >= s && d <= e;
+}
+
+const WEEKDAYS = ['Mo','Di','Mi','Do','Fr','Sa','So'];
+const MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
+
+export default function CalendarWidget() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [today] = useState(new Date());
+ const [cur, setCur] = useState({ y: today.getFullYear(), m: today.getMonth() });
+ const [events, setEvents] = useState([]);
+ const [feeds, setFeeds] = useState(() => {
+ try { return JSON.parse(localStorage.getItem('dd_cal_feeds') || '[]'); } catch { return []; }
+ });
+ const [loading, setLoading] = useState(false);
+ const [selected, setSelected] = useState(null); // date object
+
+ // Load all feeds
+ const loadFeeds = async (feedList) => {
+ if (!feedList.length) { setEvents([]); return; }
+ setLoading(true);
+ const all = [];
+ for (const f of feedList) {
+ try {
+ const res = await fetch(`/api/calendar/fetch?url=${encodeURIComponent(f.url)}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` }
+ });
+ if (res.ok) {
+ const text = await res.text();
+ const evs = parseIcal(text).map(e => ({ ...e, color: f.color, feedName: f.name }));
+ all.push(...evs);
+ }
+ } catch {}
+ }
+ setEvents(all);
+ setLoading(false);
+ };
+
+ useEffect(() => { if (isOpen) loadFeeds(feeds); }, [isOpen]);
+
+ // Calendar grid
+ const days = useMemo(() => {
+ const first = new Date(cur.y, cur.m, 1);
+ const dow = (first.getDay() + 6) % 7; // 0=Mo
+ const total = new Date(cur.y, cur.m+1, 0).getDate();
+ const grid = [];
+ for (let i = 0; i < dow; i++) grid.push(null);
+ for (let d = 1; d <= total; d++) grid.push(new Date(cur.y, cur.m, d));
+ return grid;
+ }, [cur]);
+
+ const eventsOnDay = (day) => day ? events.filter(e => inRange(e, day)) : [];
+ const selectedEvents = selected ? events.filter(e => inRange(e, selected)) : [];
+
+ const prevMonth = () => setCur(p => p.m === 0 ? { y:p.y-1, m:11 } : { y:p.y, m:p.m-1 });
+ const nextMonth = () => setCur(p => p.m === 11 ? { y:p.y+1, m:0 } : { y:p.y, m:p.m+1 });
+
+ return (
+
+ {/* Header */}
+
+
+ {isOpen && (
+
+ {/* Monat Nav */}
+
+
+
+ {MONTHS[cur.m]} {cur.y}
+
+
+
+
+ {/* Wochentage */}
+
+ {WEEKDAYS.map(d => (
+
{d}
+ ))}
+
+
+ {/* Tage */}
+
+ {days.map((day, i) => {
+ const evs = eventsOnDay(day);
+ const isToday = day && sameDay(day, today);
+ const isSel = day && selected && sameDay(day, selected);
+ return (
+
+ );
+ })}
+
+
+ {/* Ausgewählter Tag Events */}
+ {selected && (
+
+
+ {selected.toLocaleDateString('de-DE',{weekday:'long',day:'numeric',month:'long'})}
+
+ {selectedEvents.length === 0
+ ?
Keine Termine
+ : selectedEvents.map((e,i) => (
+
+
+
+
{e.summary}
+ {e.location &&
{e.location}
}
+ {!e.allDay &&
+ {e.dtstart.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}
+ {' – '}{e.dtend.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}
+
}
+
{e.feedName}
+
+
+ ))
+ }
+
+ )}
+
+ {feeds.length === 0 && (
+
+ Noch keine Kalender abonniert. Füge Kalender in den Einstellungen hinzu.
+
+ )}
+
+ )}
+
+ );
+}
+
+// ── iCal-Einstellungen (für Einstellungen-Seite) ──────────────────────────────
+export function CalendarSettings({ toast }) {
+ const [feeds, setFeeds] = useState(() => {
+ try { return JSON.parse(localStorage.getItem('dd_cal_feeds') || '[]'); } catch { return []; }
+ });
+ const [form, setForm] = useState({ name:'', url:'', color:'#4ecdc4' });
+ const [testing, setTesting] = useState(false);
+
+ const save = (newFeeds) => {
+ setFeeds(newFeeds);
+ localStorage.setItem('dd_cal_feeds', JSON.stringify(newFeeds));
+ };
+
+ const add = async () => {
+ if (!form.name.trim() || !form.url.trim()) { toast('Name und URL erforderlich', 'error'); return; }
+ setTesting(true);
+ try {
+ const res = await fetch(`/api/calendar/fetch?url=${encodeURIComponent(form.url)}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` }
+ });
+ if (!res.ok) { const d=await res.json(); toast(d.error||'Feed-Fehler','error'); return; }
+ save([...feeds, { ...form, id: Date.now() }]);
+ setForm({ name:'', url:'', color:'#4ecdc4' });
+ toast('Kalender hinzugefügt ✓');
+ } catch(e) { toast(e.message,'error'); } finally { setTesting(false); }
+ };
+
+ const remove = (id) => save(feeds.filter(f => f.id !== id));
+
+ const COLORS = ['#4ecdc4','#ff6b9d','#ffe66d','#6bcb77','#a78bfa','#60a5fa','#fb923c'];
+
+ return (
+
+
+
+
setForm(f=>({...f,name:e.target.value}))}
+ placeholder="z.B. Google Kalender" style={{ ...S.inp, fontSize:15, marginBottom:8 }}/>
+
+
setForm(f=>({...f,url:e.target.value}))}
+ placeholder="https://calendar.google.com/calendar/ical/…"
+ style={{ ...S.inp, fontSize:13, marginBottom:8 }}
+ autoCapitalize="none" autoCorrect="off"/>
+
+
+ {COLORS.map(col => (
+
+
+
+
+ {feeds.length > 0 && (
+
+
ABONNIERTE KALENDER
+ {feeds.map(f => (
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}