oxios 1.12.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import type { CalendarEvent } from '@/types/calendar'

// ─── Date helpers (self-contained, no external deps) ────────────────────

function isSameDay(a: Date, b: Date): boolean {
  return (
    a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate()
  )
}

/** `YYYY-MM-DD` key in the date's own local time (no TZ shift). */
function formatDateKey(d: Date): string {
  const pad = (n: number) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}

/** Build a 6×7 grid (42 cells) for the month containing `viewAnchor`. */
function buildMonthGrid(viewAnchor: Date): Date[] {
  const first = new Date(viewAnchor.getFullYear(), viewAnchor.getMonth(), 1)
  const start = new Date(first)
  start.setDate(start.getDate() - first.getDay()) // back to Sunday
  const cells: Date[] = []
  for (let i = 0; i < 42; i++) {
    const d = new Date(start)
    d.setDate(start.getDate() + i)
    cells.push(d)
  }
  return cells
}

function formatMonthYear(d: Date, locale: string): string {
  return d.toLocaleDateString(locale, { year: 'numeric', month: 'long' })
}

/** Group events by their start-date key. */
function groupByDate(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
  const map = new Map<string, CalendarEvent[]>()
  for (const ev of events) {
    const key = formatDateKey(new Date(ev.start))
    const arr = map.get(key)
    if (arr) arr.push(ev)
    else map.set(key, [ev])
  }
  return map
}

/** Source → indicator dot color. */
const SOURCE_COLOR: Record<CalendarEvent['source'], string> = {
  agent: 'bg-info',
  cron: 'bg-warning',
  user: 'bg-primary',
}

// ─── Component ────────────────────────────────────────────────────────────

interface MiniCalendarProps {
  events: CalendarEvent[]
  /** First day-of-month anchor controlling which month is displayed. */
  viewAnchor: Date
  /** Called when the user navigates to a different month. */
  onViewAnchorChange: (date: Date) => void
  /** Currently selected day (controls highlight). */
  selectedDate: Date
  /** Called when a day cell is clicked. */
  onSelectDate: (date: Date) => void
}

/**
 * Compact month calendar for the Notification Center schedule tab.
 *
 * Fully controlled: the parent owns `viewAnchor` (month) and `selectedDate`
 * (day) so the event query range stays in sync with the displayed month.
 */
export function MiniCalendar({
  events,
  viewAnchor,
  onViewAnchorChange,
  selectedDate,
  onSelectDate,
}: MiniCalendarProps) {
  const { t, i18n } = useTranslation()

  const dayLabels = [
    t('calendar.daySun'),
    t('calendar.dayMon'),
    t('calendar.dayTue'),
    t('calendar.dayWed'),
    t('calendar.dayThu'),
    t('calendar.dayFri'),
    t('calendar.daySat'),
  ]

  const eventsByDate = useMemo(() => groupByDate(events), [events])
  const cells = useMemo(() => buildMonthGrid(viewAnchor), [viewAnchor])
  const today = new Date()
  const inMonth = (d: Date) => d.getMonth() === viewAnchor.getMonth()

  const shiftMonth = (delta: number) => {
    onViewAnchorChange(new Date(viewAnchor.getFullYear(), viewAnchor.getMonth() + delta, 1))
  }

  return (
    <div className="select-none">
      {/* Header: month label + nav */}
      <div className="flex items-center justify-between mb-2">
        <span className="text-sm font-medium">{formatMonthYear(viewAnchor, i18n.language)}</span>
        <div className="flex items-center gap-0.5">
          <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => shiftMonth(-1)}>
            <ChevronLeft className="h-3.5 w-3.5" />
          </Button>
          <Button
            variant="ghost"
            size="sm"
            className="h-6 px-2 text-xs"
            onClick={() => {
              const now = new Date()
              onViewAnchorChange(now)
              onSelectDate(now)
            }}
          >
            {t('calendar.today')}
          </Button>
          <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => shiftMonth(1)}>
            <ChevronRight className="h-3.5 w-3.5" />
          </Button>
        </div>
      </div>

      {/* Day-of-week headers */}
      <div className="grid grid-cols-7 gap-px mb-1">
        {dayLabels.map((d) => (
          <div key={d} className="text-center text-2xs font-medium text-muted-foreground/70 py-1">
            {d}
          </div>
        ))}
      </div>

      {/* Day grid */}
      <div className="grid grid-cols-7 gap-px">
        {cells.map((date) => {
          const key = formatDateKey(date)
          const dayEvents = eventsByDate.get(key) ?? []
          const isToday = isSameDay(date, today)
          const isSelected = isSameDay(date, selectedDate)
          const dim = !inMonth(date)
          return (
            <button
              key={key}
              type="button"
              onClick={() => onSelectDate(date)}
              className={cn(
                'relative flex flex-col items-center justify-center aspect-square rounded-md text-xs transition-colors',
                'hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
                dim && 'text-muted-foreground/40',
                isSelected && 'bg-primary/15 text-primary font-medium',
                !isSelected && isToday && 'ring-1 ring-inset ring-primary/40',
              )}
            >
              <span>{date.getDate()}</span>
              {dayEvents.length > 0 && (
                <span className="mt-0.5 flex gap-0.5">
                  {dayEvents.slice(0, 3).map((ev) => (
                    <span
                      key={ev.uid}
                      className={cn('h-1 w-1 rounded-full', SOURCE_COLOR[ev.source])}
                    />
                  ))}
                </span>
              )}
            </button>
          )
        })}
      </div>
    </div>
  )
}