feat(calendar): Phase 3 – Weboberfläche mit Monats-, Wochen- und Listenansicht

Neue Seite CalendarPage (Menüpunkt 'Calendar' in der Seitenleiste,
zwischen Home und Papierkorb) mit Kalenderliste zum Anlegen/Umbenennen/
Löschen/Ein- und Ausblenden, Export/Import als .ics über die
Phase-2-API, und Termine anlegen/ändern/löschen über einen neuen
JSON-Endpunkt.

Backend: neuer events_api.go-Handler unter /api/v1/calendars/{id}/events
(Session-Auth) neben dem bestehenden CalDAV-Protokoll – ein Termin, den
die Web-Oberfläche anlegt, ist ein ganz normales CalDAV-Objekt und
synchronisiert genauso zu DAVx5 wie umgekehrt. Ein per CalDAV mit RRULE
angelegter, wiederkehrender Termin zeigt sich hier nur (recurring:true),
lässt sich aber bewusst nicht über dieses vereinfachte Formular
bearbeiten (409) – das würde die Wiederholungsregel eher beschädigen
als korrekt anpassen.

Gegen den laufenden Container getestet: Kalender/Termin anlegen, per
Web-API ändern und löschen, ein per CalDAV mit RRULE angelegter
Wiederkehrer taucht als recurring:true auf und blockt PATCH mit 409.
TypeScript-Build (tsc + vite build) sauber.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 5 committed 2026-09-03 23:14:28 +02:00
1 parent 4d635fff20
commit 27a026421f
10 files changed
+1163 -6

No files matched your search

@@ -0,0 +1,78 @@
import { useRef, useState, type FormEvent } from 'react'
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
import type { CalendarItem } from '../../api/client'
const palette = ['#4285f4', '#ea4335', '#fbbc04', '#34a853', '#a142f4', '#ff6d00', '#00acc1', '#607d8b']
interface Props {
calendar?: CalendarItem
onSave: (name: string, color: string) => void
onCancel: () => void
}
export default function CalendarDialog({ calendar, onSave, onCancel }: Props) {
const overlay = useOverlayDismiss(onCancel)
const inputRef = useRef<HTMLInputElement>(null)
const [name, setName] = useState(calendar?.name ?? '')
const [color, setColor] = useState(calendar?.color ?? palette[0])
function handleSubmit(e: FormEvent) {
e.preventDefault()
if (!name.trim()) return
onSave(name.trim(), color)
}
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.6)', padding: 16 }} {...overlay}>
<form onSubmit={handleSubmit} style={{
background: 'var(--color-surface)', border: '1px solid var(--color-border)',
borderRadius: 14, padding: 24, width: 340, maxWidth: '100%',
}}>
<h2 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 600, color: 'var(--color-text)' }}>
{calendar ? 'Kalender bearbeiten' : 'Neuer Kalender'}
</h2>
<label style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 4, display: 'block' }}>Name</label>
<input
ref={inputRef} type="text" value={name} onChange={e => setName(e.target.value)}
autoFocus
style={{
width: '100%', boxSizing: 'border-box', marginBottom: 16,
background: 'var(--color-surface-2)', border: '1px solid var(--color-border)',
borderRadius: 8, padding: '8px 10px', fontSize: 13.5, color: 'var(--color-text)',
}}
/>
<label style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 8, display: 'block' }}>Farbe</label>
<div style={{ display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
{palette.map(c => (
<button
key={c} type="button" onClick={() => setColor(c)}
aria-label={c}
style={{
width: 28, height: 28, borderRadius: '50%', background: c, cursor: 'pointer',
border: color === c ? '2px solid var(--color-text)' : '2px solid transparent',
boxShadow: color === c ? '0 0 0 2px var(--color-surface)' : 'none',
}}
/>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button type="button" onClick={onCancel} style={{
padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)',
background: 'transparent', color: 'var(--color-text-muted)', fontSize: 13, cursor: 'pointer',
}}>
Abbrechen
</button>
<button type="submit" style={{
padding: '8px 16px', borderRadius: 8, border: 'none',
background: 'var(--color-primary)', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer',
}}>
Speichern
</button>
</div>
</form>
</div>
)
}