calendar/frontend/src/components/views/MonthGrid.tsx
jtricerolph ca0dc9b070 Add recurring events, push notifications, config sync — and mobile layout fixes
Recurring event editing (rrule) with this/all occurrence scope, web push
subscriptions (VAPID) with a cached config layer for notification settings,
and email delivery via nodemailer.

Also fixes the calendar view never actually stacking on mobile: the
Calendars filter column used flex:1 with minWidth:0 on its sibling, so
flex-wrap never triggered regardless of viewport width, squeezing the grid
and view switcher into a sliver next to a fixed 220px sidebar. Adds a
proper mobile breakpoint that stacks the layout, scrolls the week grid
horizontally instead of compressing it, and enlarges touch targets.
2026-07-25 23:04:59 +00:00

51 lines
1.8 KiB
TypeScript

import type { EventSummary } from '../../types'
import { monthGridDays, isSameDay, eventOccursOnDay, formatTime, WEEKDAY_LABELS } from '../../dateUtils'
const MAX_VISIBLE = 3
export default function MonthGrid({ events, date, onSelectDate, onSelectEvent }: {
events: EventSummary[]
date: Date
onSelectDate?: (d: Date) => void
onSelectEvent: (ev: EventSummary) => void
}) {
const days = monthGridDays(date)
const today = new Date()
return (
<div className="cal-grid">
{WEEKDAY_LABELS.map(d => (
<div key={d} className="cal-day-header">{d}</div>
))}
{days.map(day => {
const dayEvents = events
.filter(ev => eventOccursOnDay(ev, day))
.sort((a, b) => a.start_at.localeCompare(b.start_at))
const cls = [
'cal-day',
day.getMonth() !== date.getMonth() ? 'other-month' : '',
isSameDay(day, today) ? 'today' : '',
].filter(Boolean).join(' ')
return (
<div key={day.toISOString()} className={cls} onClick={() => onSelectDate?.(day)}>
<div className="cal-day-num">{day.getDate()}</div>
{dayEvents.slice(0, MAX_VISIBLE).map(ev => (
<span
key={ev.id}
className="cal-event-chip"
style={{ background: ev.calendar_color, color: '#fff' }}
title={ev.title}
onClick={e => { e.stopPropagation(); onSelectEvent(ev) }}
>
{!ev.all_day && `${formatTime(ev.start_at)} `}{ev.title}
</span>
))}
{dayEvents.length > MAX_VISIBLE && (
<span className="cal-event-more">+{dayEvents.length - MAX_VISIBLE} more</span>
)}
</div>
)
})}
</div>
)
}