Sechs handgeschriebene Arrays in drei Dateien, und Intl kam im ganzen Projekt nicht vor – obwohl jeder Browser es seit Jahren mitbringt. Die Doppelpflege war nicht nur lästig, sie war eine Falle: DayDialog begann seine Woche am Sonntag, die anderen am Montag, jeweils passend zu ihrer Indizierung. Beide waren für sich richtig. Wer aber eines der Arrays in eine neue Ansicht kopiert hätte, hätte sich einen Off-by-one gebaut, der nur an sieben Tagen im Jahr auffällt. Jetzt eine Ordnung (Montag zuerst, wie weekdayIndex) plus eine ausdrücklich benannte zweite Liste für alles, was direkt mit getDay() indiziert – statt eines Umrechnens am Aufrufort, wo der Off-by-one sonst entsteht. Verhaltensgleich belegt: Die erzeugten Namen stimmen zeichengenau mit den gelöschten Arrays überein, vor dem Austausch verglichen. Die Sprache ist fest auf 'en' gesetzt, nicht die des Browsers. Intl.DateTimeFormat(undefined, …) nähme die Einstellung des Rechners und setzte auf einem deutschen System „Montag" neben ein englisches „Repeat" – genau der Mischmasch von vorgestern, nur durch die Hintertür. Kommt eine Sprachwahl (IDEAS.md), ist uiLocale die eine Stelle, an der sie andockt. Fünf neue Tests, unter Mutation nachgewiesen: Nimmt man dem Sonntag seinen Platz vorn, wird der Test rot. Geprüft: tsc sauber, 46 Vitest-Tests, alle 234 Prüfungen der sieben Selbsttest-Suiten grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
import { useEffect } from 'react'
|
||
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
|
||
import type { CalendarEvent } from '../../api/client'
|
||
import { weekdayLongNamesSun, monthLongNames } from './dates'
|
||
|
||
|
||
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 'all day'
|
||
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)' }}>{weekdayLongNamesSun[day.getDay()]}</div>
|
||
<div style={{ fontSize: 17, fontWeight: 600, color: 'var(--color-text)' }}>
|
||
{day.getDate()}. {monthLongNames[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 || '(untitled)'}
|
||
{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>
|
||
)
|
||
}
|