feat(calendar): Wiederholungen, Tagesansicht, Terminfarben, Farbpunkt als Schalter
Fünf Wünsche aus dem Durchtesten: 1. Wiederholungen einstellbar (täglich/wöchentlich/monatlich/jährlich, alle N, Ende nie/nach Anzahl/an einem Datum). Der eigentliche Aufwand lag nicht im Formular, sondern im Aufklappen: eine wöchentliche Reihe erschien sonst als *ein* Eintrag. Der Server klappt sie jetzt im angefragten Zeitfenster auf (?from=&to=, rrule-go über go-icals RecurrenceSet), die Ansichten fragen ihren sichtbaren Ausschnitt an. Geprüft: 5 Termine wöchentlich, nach Umstellung auf 14-tägig korrekt neu gerechnet, Fenster einer Woche liefert genau einen. Regeln, die das Formular nicht abbildet (etwa jeder zweite Dienstag im Monat), werden weiterhin korrekt *angezeigt* -- geprüft, die vier Termine stimmen -- aber nicht zum Bearbeiten angeboten (409). Ein halb verstandenes Speichern würde die Regel sonst still beschneiden. 2. Tag wird erst geöffnet, statt sofort einen Termin anzulegen: Klick auf einen Tag zeigt dessen Termine als Liste, dort wählt man einen aus oder legt bewusst einen neuen an. 3. Eigene Farbe je Termin (COLOR nach RFC 7986), leer = Kalenderfarbe. 4. Kalenderliste ohne Kästchen: der Farbpunkt selbst ist der Schalter -- gefüllt heißt sichtbar, leerer Ring ausgeblendet. 5. Ganztägig als Schiebeschalter statt Kästchen -- es stellt das Formular um, das ist keine Nebenangabe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
105ce8de4a
commit
79a7cc6a3d
8 files changed
+795
-218
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
|
||||
import type { CalendarEvent } from '../../api/client'
|
||||
|
||||
const weekdayLong = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
|
||||
const monthLabels = [
|
||||
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
]
|
||||
|
||||
export interface DayEvent extends CalendarEvent {
|
||||
calendarId: string
|
||||
/** Endgültige Farbe: eigene Farbe des Termins, sonst die des Kalenders. */
|
||||
color: string
|
||||
calendarName: string
|
||||
startsAt: Date
|
||||
endsAt: Date | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein angeklickter Tag wird erst **geöffnet**, statt sofort einen neuen
|
||||
* Termin anzulegen.
|
||||
*
|
||||
* Vorher legte jeder Klick auf eine Tageszelle direkt einen neuen Termin an –
|
||||
* auch der Klick, mit dem man eigentlich nachsehen wollte, was an dem Tag
|
||||
* ansteht. Hier steht zuerst, was da ist; einen neuen Termin gibt es auf
|
||||
* Ansage.
|
||||
*/
|
||||
export default function DayDialog({ day, events, onPick, onCreate, onClose }: {
|
||||
day: Date
|
||||
events: DayEvent[]
|
||||
onPick: (ev: DayEvent) => void
|
||||
onCreate: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const overlay = useOverlayDismiss(onClose)
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose() }
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [onClose])
|
||||
|
||||
const timeLabel = (ev: DayEvent) => {
|
||||
if (ev.all_day) return 'ganztägig'
|
||||
const from = ev.startsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
if (!ev.endsAt) return from
|
||||
const to = ev.endsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
return `${from} – ${to}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 190, display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.6)', padding: 16,
|
||||
}} {...overlay}>
|
||||
<div style={{
|
||||
background: 'var(--color-surface)', border: '1px solid var(--color-border)',
|
||||
borderRadius: 14, width: 420, maxWidth: '100%', maxHeight: '80vh',
|
||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ padding: '18px 22px 12px', borderBottom: '1px solid var(--color-border)' }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>{weekdayLong[day.getDay()]}</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
{day.getDate()}. {monthLabels[day.getMonth()]} {day.getFullYear()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, overflowY: 'auto', flex: 1 }}>
|
||||
{events.length === 0 && (
|
||||
<p style={{ margin: '12px 10px', fontSize: 13, color: 'var(--color-text-muted)' }}>
|
||||
Nichts an diesem Tag.
|
||||
</p>
|
||||
)}
|
||||
{events.map(ev => (
|
||||
<button
|
||||
key={ev.calendarId + ev.uid + ev.start}
|
||||
onClick={() => onPick(ev)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, width: '100%',
|
||||
padding: '9px 10px', marginBottom: 4, borderRadius: 8, textAlign: 'left',
|
||||
background: 'transparent', border: '1px solid var(--color-border)', cursor: 'pointer',
|
||||
}}
|
||||
className="aether-cal-cell"
|
||||
>
|
||||
<span style={{ width: 4, alignSelf: 'stretch', borderRadius: 2, background: ev.color, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ display: 'block', fontSize: 13.5, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{ev.summary || '(ohne Titel)'}
|
||||
{ev.recurring && <span style={{ marginLeft: 6, fontSize: 11, color: 'var(--color-text-muted)' }}>↻</span>}
|
||||
</span>
|
||||
<span style={{ display: 'block', fontSize: 11.5, color: 'var(--color-text-muted)' }}>
|
||||
{timeLabel(ev)}
|
||||
{ev.location ? ` · ${ev.location}` : ''}
|
||||
{` · ${ev.calendarName}`}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '12px 16px', borderTop: '1px solid var(--color-border)' }}>
|
||||
<button onClick={onCreate} style={{
|
||||
padding: '8px 14px', borderRadius: 8, border: 'none',
|
||||
background: 'var(--color-primary)', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer',
|
||||
}}>
|
||||
+ Termin an diesem Tag
|
||||
</button>
|
||||
<button onClick={onClose} style={{
|
||||
padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)',
|
||||
background: 'transparent', color: 'var(--color-text-muted)', fontSize: 13, cursor: 'pointer',
|
||||
}}>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user