Calendar

8 blocks

Calendar 01

Preview
npx nateui@latest add CalendarUpcomingActivities
Dark
import { useState } from 'react'
import { PiClipboard, PiUser } from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar'
import Calendar from '@/components/ui/Calendar'
import Card from '@/components/ui/Card'
import Scroll from '@/components/ui/Scroll'
import Segment from '@/components/ui/Segment'
import Tag from '@/components/ui/Tag'
import UsersAvatarGroup from '@/components/composites/UsersAvatarGroup'

const assetBase = 'https://statics.nateui.com/img'

const meetingPlatformLogos = {
    'Google Meet': `${assetBase}/thumbs/brands/google-meet.png`,
    Zoom: `${assetBase}/thumbs/brands/zoom.png`,
    Slack: `${assetBase}/thumbs/brands/slack.png`,
    'Phone call': `${assetBase}/thumbs/brands/whatsapp.png`,
}

const tabs = [
    { value: 'meeting', label: 'Meetings', icon: <PiUser aria-hidden="true" /> },
    { value: 'task', label: 'Tasks', icon: <PiClipboard aria-hidden="true" /> },
]

const meetings = [
    {
        id: 'meeting-1',
        title: 'Meeting with Acme Corp',
        time: '9:00 - 9:30 AM',
        location: 'Slack',
        attendees: [
            { name: 'Angelina', img: `${assetBase}/avatars/thumb-1.jpg` },
            { name: 'Max', img: `${assetBase}/avatars/thumb-2.jpg` },
            { name: 'Eugene', img: `${assetBase}/avatars/thumb-3.jpg` },
            { name: 'Arlene', img: `${assetBase}/avatars/thumb-4.jpg` },
            { name: 'Steve', img: `${assetBase}/avatars/thumb-5.jpg` },
        ],
    },
    {
        id: 'meeting-2',
        title: 'Strategy call with Nexera',
        time: '10:30 - 11:15 AM',
        location: 'Zoom',
        attendees: [
            { name: 'Jeremiah', img: `${assetBase}/avatars/thumb-6.jpg` },
            { name: 'Shannon', img: `${assetBase}/avatars/thumb-7.jpg` },
            { name: 'Camila', img: `${assetBase}/avatars/thumb-8.jpg` },
        ],
    },
    {
        id: 'meeting-3',
        title: 'Follow-up call with Cloudora',
        time: '2:30 - 2:50 PM',
        location: 'Phone call',
        attendees: [
            { name: 'Jessica', img: `${assetBase}/avatars/thumb-9.jpg` },
            { name: 'Camila', img: `${assetBase}/avatars/thumb-8.jpg` },
        ],
    },
    {
        id: 'meeting-4',
        title: 'Product review with Acme Corp',
        time: '4:00 - 4:30 PM',
        location: 'Google Meet',
        attendees: [
            { name: 'Jackie', img: `${assetBase}/avatars/thumb-10.jpg` },
            { name: 'Miriam', img: `${assetBase}/avatars/thumb-11.jpg` },
            { name: 'Cassandra', img: `${assetBase}/avatars/thumb-12.jpg` },
            { name: 'Earl', img: `${assetBase}/avatars/thumb-13.jpg` },
        ],
    },
    {
        id: 'meeting-5',
        title: 'Renewal call with Nexera',
        time: '5:00 - 5:30 PM',
        location: 'Zoom',
        attendees: [{ name: 'Alvin', img: `${assetBase}/avatars/thumb-14.jpg` }],
    },
]

const tasks = [
    {
        id: 'task-1',
        title: 'Send proposal to Acme Corp',
        time: 'Due 4:00 PM',
        location: 'Sales pipeline',
        subject: { name: 'Eugene', img: `${assetBase}/avatars/thumb-3.jpg` },
    },
    {
        id: 'task-2',
        title: 'Update Nexera records',
        time: 'Due 5:00 PM',
        location: 'Workspace',
        subject: { name: 'Jackie', img: `${assetBase}/avatars/thumb-10.jpg` },
    },
    {
        id: 'task-3',
        title: 'Review Cloudora forecast',
        time: 'Due 6:00 PM',
        location: 'Workspace',
        subject: { name: 'Miriam', img: `${assetBase}/avatars/thumb-11.jpg` },
    },
    {
        id: 'task-4',
        title: 'Prepare Acme Corp demo',
        time: 'Due 6:30 PM',
        location: 'Sales pipeline',
        subject: { name: 'Max', img: `${assetBase}/avatars/thumb-2.jpg` },
    },
]

export default function CalendarUpcomingActivities() {
    const [activeTab, setActiveTab] = useState<'meeting' | 'task'>('meeting')

    return (
        <Card bodyClass="p-0">
            <div className="space-y-4 py-4">
                <div className="px-4">
                    <h5 className="text-lg font-semibold text-card-foreground">
                        Upcoming Activities
                    </h5>
                </div>

                <Calendar.Mini
                    visibleCount={7}
                    showHeader={false}
                    showStripNavigation={false}
                    className="border-b px-4 py-2"
                />

                <div className="space-y-4 px-4">
                    <Segment
                        className="w-full"
                        value={activeTab}
                        onChange={(value) =>
                            setActiveTab(value as 'meeting' | 'task')
                        }
                    >
                        {tabs.map((tab) => (
                            <Segment.Item
                                key={tab.value}
                                value={tab.value}
                                className="gap-2"
                            >
                                {tab.icon}
                                <span>{tab.label}</span>
                            </Segment.Item>
                        ))}
                    </Segment>

                    <Scroll className="h-80" scrollbars="vertical">
                        <div className="space-y-2 pe-1">
                            {activeTab === 'meeting'
                                ? meetings.map((activity) => {
                                      const platformLogo =
                                          meetingPlatformLogos[
                                              activity.location as keyof typeof meetingPlatformLogos
                                          ]

                                      return (
                                          <button
                                              key={activity.id}
                                              type="button"
                                              className="block w-full space-y-2 rounded-card border bg-accent/40 p-3 text-left transition-colors hover:bg-accent"
                                          >
                                              <div className="flex flex-col justify-between gap-4">
                                                  <div className="min-w-0">
                                                      <div className="truncate text-sm font-semibold text-card-foreground">
                                                          {activity.title}
                                                      </div>
                                                      <div className="mt-1 text-xs font-medium text-muted-foreground">
                                                          {activity.time}
                                                      </div>
                                                  </div>
                                                  <div className="flex items-center justify-between gap-2">
                                                      <UsersAvatarGroup
                                                          users={activity.attendees}
                                                          maxCount={3}
                                                          avatarProps={{
                                                              size: 24,
                                                              className:
                                                                  'bg-accent text-accent-foreground',
                                                          }}
                                                      />
                                                      <Tag
                                                          prefix={
                                                              <Avatar
                                                                  src={platformLogo}
                                                                  alt=""
                                                                  size={16}
                                                                  shape="square"
                                                                  className="mr-1 border-0 bg-transparent"
                                                              />
                                                          }
                                                      >
                                                          {activity.location}
                                                      </Tag>
                                                  </div>
                                              </div>
                                          </button>
                                      )
                                  })
                                : tasks.map((activity) => (
                                      <button
                                          key={activity.id}
                                          type="button"
                                          className="block w-full space-y-2 rounded-card border bg-accent/40 p-3 text-left transition-colors hover:bg-accent"
                                      >
                                          <div className="flex flex-col justify-between gap-4">
                                              <div className="min-w-0">
                                                  <div className="truncate text-sm font-semibold text-card-foreground">
                                                      {activity.title}
                                                  </div>
                                                  <div className="mt-1 text-xs font-medium text-muted-foreground">
                                                      {activity.time}
                                                  </div>
                                              </div>
                                              <div className="flex items-center justify-between gap-2">
                                                  <div className="flex min-w-0 items-center gap-2">
                                                      <Avatar
                                                          src={activity.subject.img}
                                                          alt=""
                                                          size={24}
                                                          className="bg-accent text-accent-foreground"
                                                      />
                                                      <span className="truncate text-sm font-medium text-card-foreground">
                                                          {activity.subject.name}
                                                      </span>
                                                  </div>
                                                  <Tag>{activity.location}</Tag>
                                              </div>
                                          </div>
                                      </button>
                                  ))}
                        </div>
                    </Scroll>
                </div>
            </div>
        </Card>
    )
}

Calendar 02

Preview
npx nateui@latest add CalendarRecurrencePanel
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import Card from '@/components/ui/Card'
import Checkbox from '@/components/ui/Checkbox'
import Form from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Radio from '@/components/ui/Radio'
import Select from '@/components/ui/Select'

type EndsMode = 'never' | 'on' | 'after'

const frequencyOptions = [
    { label: 'Daily', value: 'daily' },
    { label: 'Weekly', value: 'weekly' },
    { label: 'Monthly', value: 'monthly' },
    { label: 'Yearly', value: 'yearly' },
]

const panelTitleId = 'calendar-recurrence-panel-title'

const getStartOfToday = () => {
    const date = new Date()
    date.setHours(0, 0, 0, 0)
    return date
}

const getInitialEndDate = () => {
    const date = getStartOfToday()
    date.setDate(date.getDate() + 7)
    return date
}

const formatDate = (date: Date) => {
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    const day = String(date.getDate()).padStart(2, '0')

    return `${year}-${month}-${day}`
}

export default function CalendarRecurrencePanel() {
    const [frequency, setFrequency] = useState('daily')
    const [skipWeekends, setSkipWeekends] = useState(false)
    const [endsMode, setEndsMode] = useState<EndsMode>('on')
    const [endDate, setEndDate] = useState(() => getInitialEndDate())
    const [endDateValue, setEndDateValue] = useState(() =>
        formatDate(getInitialEndDate()),
    )
    const [occurrences, setOccurrences] = useState('10')

    const today = getStartOfToday()

    const handleCalendarChange = (date: Date | null) => {
        if (!date) {
            return
        }

        setEndDate(date)
        setEndDateValue(formatDate(date))
        setEndsMode('on')
    }

    return (
        <section aria-labelledby={panelTitleId}>
            <Card className="overflow-hidden" bodyClass="p-0">
                <div className="flex flex-col sm:flex-row">
                    <div className="flex min-w-0 flex-col flex-1">
                        <header className="flex items-center justify-between gap-4 p-4">
                            <h5 id={panelTitleId}>
                                Recurrence settings
                            </h5>
                        </header>

                        <Form
                            className="flex flex-1 flex-col"
                            containerClassName="flex flex-1 flex-col"
                            onSubmit={(event) => event.preventDefault()}
                        >
                            <div className="flex flex-1 flex-col gap-4 p-4">
                                <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                                    <Form.Field
                                        className="mb-0"
                                        htmlFor="calendar-recurrence-frequency"
                                        label="Frequency"
                                    >
                                        <Select
                                            className="mt-2"
                                            inputId="calendar-recurrence-frequency"
                                            options={frequencyOptions}
                                            value={
                                                frequencyOptions.find(
                                                    (option) =>
                                                        option.value === frequency,
                                                ) ?? frequencyOptions[0]
                                            }
                                            onChange={(option) =>
                                                setFrequency(option.value as string)
                                            }
                                        />
                                    </Form.Field>

                                    <div className="flex items-center sm:pt-8">
                                        <Checkbox
                                            checked={skipWeekends}
                                            onChange={(checked) =>
                                                setSkipWeekends(checked)
                                            }
                                        >
                                            Skip weekends
                                        </Checkbox>
                                    </div>
                                </div>

                                <fieldset>
                                    <legend className="sr-only">Ends</legend>
                                    <Form.Field
                                        className="mb-0"
                                        label="Ends"
                                        labelId="calendar-recurrence-ends-label"
                                    >
                                        <Radio.Group
                                            aria-labelledby="calendar-recurrence-ends-label"
                                            className="grid w-full grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-2 gap-y-2 mt-2"
                                            name="calendar-recurrence-ends"
                                            value={endsMode}
                                            onChange={(value) =>
                                                setEndsMode(value as EndsMode)
                                            }
                                        >
                                            <Radio value="never">Never</Radio>
                                            <div aria-hidden="true" />
                                            <Radio value="on">On date</Radio>
                                            <Input
                                                aria-label="Recurrence end date"
                                                disabled={endsMode !== 'on'}
                                                placeholder="YYYY-MM-DD"
                                                type="text"
                                                value={endDateValue}
                                                onChange={(event) =>
                                                    setEndDateValue(
                                                        event.target.value,
                                                    )
                                                }
                                            />
                                            <Radio value="after">After</Radio>
                                            <Input
                                                aria-label="Number of occurrences"
                                                disabled={endsMode !== 'after'}
                                                min={1}
                                                suffix="occurrences"
                                                type="number"
                                                value={occurrences}
                                                onChange={(event) =>
                                                    setOccurrences(
                                                        event.target.value,
                                                    )
                                                }
                                            />
                                        </Radio.Group>
                                    </Form.Field>
                                </fieldset>
                            </div>

                            <footer className="flex flex-wrap justify-end gap-2 p-4">
                                <Button
                                    type="button"
                                    variant="default"
                                >
                                    Cancel
                                </Button>
                                <Button
                                    type="button"
                                    variant="solid"
                                >
                                    Save
                                </Button>
                            </footer>
                        </Form>
                    </div>

                    <div className="flex justify-center items-center min-w-0 flex-col bg-muted p-4 sm:max-w-[283px]">
                        <Calendar
                            aria-label="Select recurrence end date"
                            className="w-full"
                            firstDayOfWeek="sunday"
                            minDate={today}
                            value={endDate}
                            onChange={handleCalendarChange}
                        />
                    </div>
                </div>
            </Card>
        </section>
    )
}

Calendar 03

Preview
npx nateui@latest add CalendarBookingScheduler
Dark
import { useState } from 'react'
import { PiClockFill, PiVideoCameraFill, PiTranslateBold, PiUserFill } from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import Card from '@/components/ui/Card'
import Notification from '@/components/ui/Notification'
import Scroll from '@/components/ui/Scroll'
import toast from '@/components/ui/toast'

const eventTitleId = 'calendar-booking-scheduler-event-title'

const availableDays = new Set([10, 11, 15, 17, 22, 24, 29])
const fixtureMonth = new Date(2026, 8, 1)
const initialDate = new Date(2026, 8, 10)

const timezoneOptions = [
    { label: 'UTC-05:00 Eastern Time', value: 'utc-05' },
    { label: 'UTC+00:00 Coordinated Universal Time', value: 'utc-00' },
    { label: 'UTC+08:00 Kuala Lumpur', value: 'utc+08' },
]

const timeSlots = [
    '09:00 AM',
    '09:30 AM',
    '10:00 AM',
    '10:30 AM',
    '11:00 AM',
    '01:00 PM',
    '01:30 PM',
    '02:00 PM',
    '02:30 PM',
    '03:00 PM',
]

const eventDetails = [
    { value: '30 minutes', Icon: PiClockFill },
    { value: 'Video call', Icon: PiVideoCameraFill },
    { value: 'English', Icon: PiTranslateBold },
] as const

const isAvailableDate = (date: Date) =>
    date.getFullYear() === fixtureMonth.getFullYear() &&
    date.getMonth() === fixtureMonth.getMonth() &&
    availableDays.has(date.getDate())

const formatSelectedDate = (date: Date) =>
    new Intl.DateTimeFormat('en-US', {
        weekday: 'long',
        month: 'short',
        day: 'numeric',
    }).format(date)

export default function CalendarBookingScheduler() {
    const [selectedDate, setSelectedDate] = useState(initialDate)
    const [selectedSlot, setSelectedSlot] = useState<string | null>(null)

    const handleDateChange = (date: Date | null) => {
        if (!date) {
            return
        }

        setSelectedDate(date)
        setSelectedSlot(null)
    }

    const handleConfirm = () => {
        toast.push(
            <Notification type="success" title="Book successful" />,
            { placement: 'top-center' },
        )
    }

    return (
        <section className="py-16 mx-auto max-w-240" aria-labelledby={eventTitleId}>
            <Card className="overflow-hidden" bodyClass="p-0">
                <div className="flex flex-col md:flex-row md:flex-wrap lg:flex-nowrap">
                    <div className="flex min-w-0 flex-col gap-4 border-b p-4 md:flex-1 md:border-b-0 md:border-r lg:basis-0">
                        <div className="flex flex-col gap-2">
                            <div className="flex items-center gap-2 mb-4">
                                <Avatar 
                                    aria-hidden="true" 
                                    size="sm"
                                    icon={
                                        <PiUserFill className="text-muted-foreground" />
                                    }
                                />
                                <p className="text-medium">
                                    Avery Lin
                                </p>
                            </div>
                            <h4 id={eventTitleId}>Planning session</h4>
                        </div>

                        <dl className="flex flex-col gap-4 mt-2">
                            {eventDetails.map(({ value, Icon }) => (
                                <div
                                    key={value}
                                    className="flex items-center gap-2"
                                >
                                    <Icon
                                        aria-hidden="true"
                                        className="shrink-0 text-lg text-muted-foreground"
                                    />
                                    <dd className="font-medium">{value}</dd>
                                </div>
                            ))}
                        </dl>

                        <div className="flex flex-col gap-2 text-muted-foreground">
                            <p>A focused slot for aligning the next project step. Bring the open questions you want to resolve.</p>
                        </div>
                    </div>

                    <div className="flex min-w-0 flex-col gap-4 border-b p-4 md:flex-1 md:border-b-0 lg:max-w-[300px] lg:border-r">
                        <h5 id="calendar-booking-scheduler-date-heading">
                            Choose a date
                        </h5>

                        <Calendar
                            aria-label="Select an appointment date"
                            className="w-full"
                            defaultMonth={fixtureMonth}
                            disableDate={(date) => !isAvailableDate(date)}
                            firstDayOfWeek="sunday"
                            value={selectedDate}
                            onChange={handleDateChange}
                        />
                    </div>

                    <div className="flex min-h-96 min-w-0 flex-col gap-4 p-4 md:basis-full md:min-h-0 md:border-t lg:basis-0 lg:flex-1 lg:border-t-0 lg:max-w-[300px]">
                        <h5 id="calendar-booking-scheduler-slots-heading">
                            {formatSelectedDate(selectedDate)}
                        </h5>

                        <Scroll
                            className="min-h-0 flex-1"
                            scrollbars="vertical"
                        >
                            <div className="flex flex-col gap-2">
                                {timeSlots.map((slot) => {
                                    const isSelected = selectedSlot === slot

                                    return (
                                        <div
                                            key={slot}
                                            className={
                                                isSelected
                                                    ? 'grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2'
                                                    : 'min-w-0'
                                            }
                                        >
                                            {isSelected ? (
                                                <div className="flex h-control-md min-w-0 items-center justify-center rounded-control bg-secondary px-4 font-medium text-foreground">
                                                    {slot}
                                                </div>
                                            ) : (
                                                <Button
                                                    className="w-full min-w-0"
                                                    type="button"
                                                    variant="default"
                                                    onClick={() =>
                                                        setSelectedSlot(slot)
                                                    }
                                                >
                                                    {slot}
                                                </Button>
                                            )}
                                            {isSelected && (
                                                <Button
                                                    aria-label={`Confirm ${slot} time`}
                                                    type="button"
                                                    variant="solid"
                                                    onClick={handleConfirm}
                                                >
                                                    Confirm
                                                </Button>
                                            )}
                                        </div>
                                    )
                                })}
                            </div>
                        </Scroll>
                    </div>
                </div>
            </Card>
        </section>
    )
}

Calendar 04

Preview
npx nateui@latest add CalendarTaskPlanner
Dark
import { useState } from 'react'
import { PiClipboardTextFill, PiFlagFill, PiPlus, PiVideoCameraFill } from 'react-icons/pi'
import Tag from  '@/components/ui/Tag'
import IconFrame from '@/components/composites/IconFrame'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import Card from '@/components/ui/Card'

const headingId = 'calendar-task-planner-heading'

const taskTypes = {
    meeting: {
        label: 'Meeting',
        Icon: PiVideoCameraFill,
        dotClass: 'bg-palette-purple',
        frameClass: 'bg-palette-purple-soft',
        iconClass: 'text-palette-purple-soft-foreground',
    },
    review: {
        label: 'Review',
        Icon: PiClipboardTextFill,
        dotClass: 'bg-palette-emerald',
        frameClass: 'bg-palette-emerald-soft',
        iconClass: 'text-palette-emerald-soft-foreground',
    },
    deadline: {
        label: 'Deadline',
        Icon: PiFlagFill,
        dotClass: 'bg-palette-rose',
        frameClass: 'bg-palette-rose-soft',
        iconClass: 'text-palette-rose-soft-foreground',
    },
}

type TaskType = keyof typeof taskTypes

const anchorMonth = new Date(2026, 7, 1)
const anchorDate = new Date(2026, 7, 12)

const monthMarkers: Record<number, TaskType> = {
    4: 'review',
    7: 'meeting',
    12: 'meeting',
    15: 'deadline',
    19: 'review',
    22: 'meeting',
    27: 'deadline',
}

const tasks: { id: string; type: TaskType; title: string; time: string }[] = [
    {
        id: 'task-1',
        type: 'meeting',
        title: 'Weekly platform sync',
        time: '9:30 - 10:15 AM',
    },
    {
        id: 'task-2',
        type: 'review',
        title: 'Design walkthrough',
        time: '11:00 - 11:45 AM',
    },
    {
        id: 'task-3',
        type: 'deadline',
        title: 'Vendor invoice cutoff',
        time: '1:00 - 1:30 PM',
    },
    {
        id: 'task-4',
        type: 'meeting',
        title: 'New hire introduction',
        time: '3:00 - 3:45 PM',
    },
]

export default function CalendarTaskPlanner() {
    const [selectedDate, setSelectedDate] = useState(anchorDate)
    const [visibleMonth, setVisibleMonth] = useState(anchorMonth)

    const renderDay = (date: Date) => {
        const inVisibleMonth =
            date.getMonth() === visibleMonth.getMonth() &&
            date.getFullYear() === visibleMonth.getFullYear()
        const marker = inVisibleMonth ? monthMarkers[date.getDate()] : undefined
        const isSelected =
            date.getDate() === selectedDate.getDate() &&
            date.getMonth() === selectedDate.getMonth() &&
            date.getFullYear() === selectedDate.getFullYear()

        const dotClass = isSelected
            ? 'bg-primary-foreground'
            : marker && taskTypes[marker].dotClass

        return (
            <span className="relative flex h-full w-full items-center justify-center">
                <span>{date.getDate()}</span>
                {marker && (
                    <span
                        aria-hidden="true"
                        className={`absolute inset-x-0 bottom-1.5 mx-auto h-1 w-1 rounded-full ${dotClass}`}
                    />
                )}
            </span>
        )
    }

    return (
        <section aria-labelledby={headingId}>
            <Card className="overflow-hidden" bodyClass="p-0">
                <div className="p-4">
                    <h5 id={headingId}>Task planner</h5>
                    <Calendar
                        className="mt-4"
                        defaultMonth={anchorMonth}
                        firstDayOfWeek="sunday"
                        renderDay={renderDay}
                        value={selectedDate}
                        onChange={(date) => date && setSelectedDate(date)}
                        onMonthChange={setVisibleMonth}
                    />
                </div>

                <div className="flex items-center justify-between gap-2 border-t px-4 py-3">
                    <h6 className="min-w-0 truncate">
                        Today events
                    </h6>
                    <Button
                        aria-label="Add task"
                        className="shrink-0"
                        icon={<PiPlus />}
                        size="sm"
                        variant="subtle"
                    />
                </div>

                <ul className="divide-y border-t">
                    {tasks.map((task) => {
                        const { label, Icon, frameClass, iconClass } =
                            taskTypes[task.type]

                        return (
                            <li
                                key={task.id}
                                className="flex items-center justify-between px-4 py-4"
                            >
                                <div className="flex items-center  gap-2">
                                    <IconFrame
                                        className={`shrink-0 ${frameClass}`}
                                        size={36}
                                        variant="muted"
                                    >
                                        <Icon
                                            aria-hidden="true"
                                            className={`text-lg ${iconClass}`}
                                        />
                                    </IconFrame>
                                    <div className="min-w-0">
                                        <div className="font-semibold">
                                            {task.title}
                                        </div>
                                        <div className="text-xs text-muted-foreground">
                                            {task.time}
                                        </div>
                                    </div>
                                </div>
                                <Tag >
                                    {label}
                                </Tag>
                            </li>
                        )
                    })}
                </ul>
            </Card>
        </section>
    )
}

Calendar 05

Preview
npx nateui@latest add CalendarEventWorkspace
Dark
import { useState } from 'react'
import { PiCheck, PiSlidersHorizontal, PiPlus } from 'react-icons/pi'
import FullCalendar from '@/components/composites/FullCalendar'
import Button from '@/components/ui/Button'
import DatePicker from '@/components/ui/DatePicker'
import Dropdown from '@/components/ui/Dropdown'
import Drawer from '@/components/ui/Drawer'
import Form from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import TimeInput from '@/components/ui/TimeInput'
import type {
    CalendarView,
    FullCalendarEvent,
} from '@/components/composites/FullCalendar'

const headingId = 'calendar-event-workspace-heading'
const createEventFormId = 'calendar-event-form'

const swatchClass: Record<string, string> = {
    blue: 'bg-palette-blue',
    purple: 'bg-palette-purple',
    green: 'bg-palette-emerald',
    red: 'bg-palette-red',
}

const calendars = [
    { id: 'product', label: 'Product', color: 'blue' },
    { id: 'marketing', label: 'Marketing', color: 'purple' },
    { id: 'team', label: 'Team', color: 'green' },
    { id: 'deadlines', label: 'Deadlines', color: 'red' },
]

const viewOptions: { label: string; value: CalendarView }[] = [
    { label: 'Month', value: 'month' },
    { label: 'Week', value: 'week' },
    { label: 'Day', value: 'day' },
    { label: 'Agenda', value: 'agenda' },
]

const events = [
    {
        id: 1,
        type: 'product',
        color: 'blue',
        title: 'Roadmap sync',
        description: 'Quarterly roadmap review with the product team.',
        startDate: '2026-08-03T09:00:00',
        endDate: '2026-08-03T09:45:00',
    },
    {
        id: 2,
        type: 'marketing',
        color: 'purple',
        title: 'Content review',
        description: 'Review drafts before the newsletter goes out.',
        startDate: '2026-08-05T13:00:00',
        endDate: '2026-08-05T13:30:00',
    },
    {
        id: 3,
        type: 'team',
        color: 'green',
        title: 'Design critique',
        description: 'Walkthrough of the latest screens with feedback.',
        startDate: '2026-08-12T10:00:00',
        endDate: '2026-08-12T10:30:00',
    },
    {
        id: 4,
        type: 'deadlines',
        color: 'red',
        title: 'Invoice due',
        description: 'Vendor invoice payment window closes.',
        startDate: '2026-08-12T17:00:00',
        endDate: '2026-08-12T17:30:00',
    },
    {
        id: 5,
        type: 'product',
        color: 'blue',
        title: 'API review',
        description: 'Walk through the proposed endpoint changes.',
        startDate: '2026-08-21T11:00:00',
        endDate: '2026-08-21T11:30:00',
    },
    {
        id: 6,
        type: 'marketing',
        color: 'purple',
        title: 'Newsletter draft',
        description: 'First pass on next month’s newsletter copy.',
        startDate: '2026-08-26T15:00:00',
        endDate: '2026-08-26T15:30:00',
    },
    {
        id: 7,
        type: 'product',
        color: 'blue',
        title: 'Onsite workshop',
        description: 'Cross-team planning workshop at the office.',
        startDate: '2026-08-14T00:00:00',
        endDate: '2026-08-17T00:00:00',
    },
    {
        id: 8,
        type: 'team',
        color: 'green',
        title: 'Team offsite',
        description: 'Annual offsite for the wider team.',
        startDate: '2026-08-24T00:00:00',
        endDate: '2026-08-26T00:00:00',
    },
    {
        id: 9,
        type: 'product',
        color: 'blue',
        title: 'Standup',
        description: 'Daily standup with the product squad.',
        startDate: '2026-08-19T08:30:00',
        endDate: '2026-08-19T09:00:00',
    },
    {
        id: 10,
        type: 'marketing',
        color: 'purple',
        title: 'Campaign review',
        description: 'Check performance on the current campaign.',
        startDate: '2026-08-19T09:30:00',
        endDate: '2026-08-19T10:15:00',
    },
    {
        id: 11,
        type: 'team',
        color: 'green',
        title: '1:1 check-in',
        description: 'Regular check-in with a direct report.',
        startDate: '2026-08-19T11:00:00',
        endDate: '2026-08-19T11:30:00',
    },
    {
        id: 12,
        type: 'deadlines',
        color: 'red',
        title: 'Contract signature',
        description: 'Countersign the vendor renewal contract.',
        startDate: '2026-08-19T12:00:00',
        endDate: '2026-08-19T12:15:00',
    },
    {
        id: 13,
        type: 'product',
        color: 'blue',
        title: 'Sprint planning',
        description: 'Plan scope for the upcoming sprint.',
        startDate: '2026-08-19T14:00:00',
        endDate: '2026-08-19T14:45:00',
    },
    {
        id: 14,
        type: 'marketing',
        color: 'purple',
        title: 'Retro',
        description: 'Retrospective on the last campaign push.',
        startDate: '2026-08-19T16:00:00',
        endDate: '2026-08-19T16:30:00',
    },
]

type EventDraft = {
    title: string
    calendarId: string
    allDay: boolean
    startDate: Date
    endDate: Date
    startTime: Date
    endTime: Date
    description: string
}

const defaultCreateDate = new Date(2026, 7, 19)

const createDraft = (date: Date = defaultCreateDate): EventDraft => {
    const startDate = new Date(date)
    startDate.setHours(0, 0, 0, 0)

    const startTime = new Date(startDate)
    if (startTime.getHours() === 0 && startTime.getMinutes() === 0) {
        startTime.setHours(9, 0, 0, 0)
    }

    const endTime = new Date(startTime)
    endTime.setMinutes(endTime.getMinutes() + 30)

    return {
        title: '',
        calendarId: calendars[0].id,
        allDay: false,
        startDate,
        endDate: new Date(startDate),
        startTime,
        endTime,
        description: '',
    }
}

const isAllDayEvent = (event: FullCalendarEvent) => {
    const start = new Date(event.startDate)
    const end = new Date(event.endDate)
    const startsAtMidnight =
        start.getHours() === 0 && start.getMinutes() === 0
    const endsAtMidnight = end.getHours() === 0 && end.getMinutes() === 0
    const endsAtDayClose =
        end.getHours() === 23 &&
        end.getMinutes() === 59 &&
        end.getSeconds() === 59

    return (
        startsAtMidnight &&
        ((endsAtMidnight &&
            end.getTime() - start.getTime() >= 24 * 60 * 60 * 1000) ||
            endsAtDayClose)
    )
}

const createDraftFromEvent = (event: FullCalendarEvent): EventDraft => {
    const start = new Date(event.startDate)
    const end = new Date(event.endDate)
    const startDate = new Date(start)
    const endDate = new Date(end)
    const allDay = isAllDayEvent(event)

    startDate.setHours(0, 0, 0, 0)
    endDate.setHours(0, 0, 0, 0)

    return {
        title: event.title,
        calendarId: event.type,
        allDay,
        startDate,
        endDate,
        startTime: start,
        endTime: end,
        description: event.description,
    }
}

const combineDateAndTime = (date: Date, time: Date) => {
    const combined = new Date(date)
    combined.setHours(time.getHours(), time.getMinutes(), 0, 0)
    return combined
}

const endOfDay = (date: Date) => {
    const end = new Date(date)
    end.setHours(23, 59, 59, 999)
    return end
}

export default function CalendarEventWorkspace() {
    const [calendarEvents, setCalendarEvents] = useState(events)
    const [isEventDrawerOpen, setEventDrawerOpen] = useState(false)
    const [editingEventId, setEditingEventId] = useState<number | null>(null)
    const [eventDraft, setEventDraft] = useState<EventDraft>(() =>
        createDraft(),
    )
    const [createError, setCreateError] = useState<string | null>(null)
    const [visibleCalendarIds, setVisibleCalendarIds] = useState(
        () => new Set(calendars.map((calendar) => calendar.id)),
    )

    const isEditingEvent = editingEventId !== null

    const openCreateDrawer = (date?: Date) => {
        setEventDraft(createDraft(date))
        setCreateError(null)
        setEditingEventId(null)
        setEventDrawerOpen(true)
    }

    const openEditDrawer = (event: FullCalendarEvent) => {
        setEventDraft(createDraftFromEvent(event))
        setCreateError(null)
        setEditingEventId(event.id)
        setEventDrawerOpen(true)
    }

    const closeEventDrawer = () => {
        setEventDrawerOpen(false)
        setEditingEventId(null)
        setCreateError(null)
    }

    const updateEventDraft = <Key extends keyof EventDraft>(
        key: Key,
        value: EventDraft[Key],
    ) => {
        setEventDraft((currentDraft) => ({
            ...currentDraft,
            [key]: value,
        }))
        setCreateError(null)
    }

    const handleSaveEvent = () => {
        const title = eventDraft.title.trim()
        const calendar = calendars.find(
            (item) => item.id === eventDraft.calendarId,
        )
        const startDateTime = combineDateAndTime(
            eventDraft.startDate,
            eventDraft.startTime,
        )
        const endDateTime = eventDraft.allDay
            ? endOfDay(eventDraft.endDate)
            : combineDateAndTime(eventDraft.endDate, eventDraft.endTime)

        if (!title) {
            setCreateError('Add a title before saving the event.')
            return
        }

        if (!calendar) {
            setCreateError('Choose a calendar before creating the event.')
            return
        }

        if (endDateTime <= startDateTime) {
            setCreateError('The end must be after the start.')
            return
        }

        setCalendarEvents((currentEvents) => {
            if (editingEventId !== null) {
                return currentEvents.map((event) =>
                    event.id === editingEventId
                        ? {
                              ...event,
                              type: calendar.id,
                              color: calendar.color,
                              title,
                              description: eventDraft.description.trim(),
                              startDate: startDateTime.toISOString(),
                              endDate: endDateTime.toISOString(),
                          }
                        : event,
                )
            }

            return [
                ...currentEvents,
                {
                    id:
                        Math.max(
                            0,
                            ...currentEvents.map(
                                (currentEvent) => currentEvent.id,
                            ),
                        ) + 1,
                    type: calendar.id,
                    color: calendar.color,
                    title,
                    description: eventDraft.description.trim(),
                    startDate: startDateTime.toISOString(),
                    endDate: endDateTime.toISOString(),
                },
            ]
        })
        setVisibleCalendarIds((currentIds) => {
            const nextIds = new Set(currentIds)
            nextIds.add(calendar.id)
            return nextIds
        })
        closeEventDrawer()
    }

    const toggleCalendar = (id: string) => {
        setVisibleCalendarIds((prev) => {
            const next = new Set(prev)
            if (next.has(id)) {
                next.delete(id)
            } else {
                next.add(id)
            }
            return next
        })
    }

    const visibleEvents = calendarEvents.filter((event) =>
        visibleCalendarIds.has(event.type),
    )

    return (
        <section  aria-labelledby={headingId}>
            <div className="w-full h-225 xl:h-screen">
                <FullCalendar
                    fillHeight
                    events={visibleEvents}
                    view="month"
                    onCellClick={openCreateDrawer}
                    onEventClick={openEditDrawer}
                    onChange={(_, updatedEvent) =>
                        setCalendarEvents((currentEvents) =>
                            currentEvents.map((event) =>
                                event.id === updatedEvent.id
                                    ? updatedEvent
                                    : event,
                            ),
                        )
                    }
                    renderMoreContent={({ overflowedEvents }) => (
                        <ul className="w-56 space-y-1">
                            {overflowedEvents.map((event) => (
                                <li
                                    key={event.id}
                                    className="flex items-center gap-2 rounded-control-sm px-2 py-1.5 hover:bg-accent"
                                >
                                    <span
                                        aria-hidden="true"
                                        className={`size-2 shrink-0 rounded-full ${swatchClass[event.color] ?? 'bg-palette-gray'}`}
                                    />
                                    <span className="min-w-0 flex-1 truncate text-xs font-medium">
                                        {event.title}
                                    </span>
                                </li>
                            ))}
                        </ul>
                    )}
                    renderHeaderEnd={({ view, setView }) => (
                        <div className="flex items-center gap-2">
                            <Dropdown
                                placement="bottom-end"
                                renderTitle={
                                    <Button
                                        variant="default"
                                        size="sm"
                                        icon={<PiSlidersHorizontal aria-hidden="true" />}
                                    >
                                        Filters
                                    </Button>
                                }
                            >
                                {calendars.map((calendar) => (
                                    <Dropdown.Item
                                        key={calendar.id}
                                        closeOnClick={false}
                                        onClick={() =>
                                            toggleCalendar(calendar.id)
                                        }
                                    >
                                        <span className="flex w-full items-center gap-2">
                                            <span
                                                aria-hidden="true"
                                                className={`size-2 shrink-0 rounded-full ${swatchClass[calendar.color]}`}
                                            />
                                            <span className="flex-1 truncate">
                                                {calendar.label}
                                            </span>
                                            {visibleCalendarIds.has(
                                                calendar.id,
                                            ) && (
                                                <PiCheck
                                                    aria-hidden="true"
                                                    className="shrink-0 text-primary"
                                                />
                                            )}
                                        </span>
                                    </Dropdown.Item>
                                ))}
                            </Dropdown>

                            <Button
                                variant="solid"
                                size="sm"
                                icon={<PiPlus aria-hidden="true" />}
                                onClick={() => openCreateDrawer()}
                            >
                                Add event
                            </Button>

                            <Select
                                className="w-32"
                                options={viewOptions}
                                value={viewOptions.find(
                                    (option) => option.value === view,
                                )}
                                onChange={(selected) => {
                                    if (selected && 'value' in selected) {
                                        setView(
                                            selected.value as CalendarView,
                                        )
                                    }
                                }}
                            />
                        </div>
                    )}
                />
            </div>
            <Drawer
                isOpen={isEventDrawerOpen}
                placement="right"
                title={isEditingEvent ? 'Edit event' : 'New event'}
                onClose={closeEventDrawer}
                footerClass="flex justify-end gap-2"
                footer={
                    <>
                        <Button
                            type="button"
                            onClick={closeEventDrawer}
                        >
                            Cancel
                        </Button>
                        <Button
                            variant="solid"
                            form={createEventFormId}
                            type="submit"
                        >
                            {isEditingEvent ? 'Save changes' : 'Create event'}
                        </Button>
                    </>
                }
            >
                <Form
                    id={createEventFormId}
                    className="space-y-4"
                    onSubmit={(event) => {
                        event.preventDefault()
                        handleSaveEvent()
                    }}
                >
                    {createError && (
                        <p
                            className="rounded-control bg-destructive-soft px-3 py-2 text-sm text-destructive"
                            role="alert"
                        >
                            {createError}
                        </p>
                    )}
                    <Form.Field
                        htmlFor="calendar-event-title"
                        label="Title"
                    >
                        <Input
                            id="calendar-event-title"
                            value={eventDraft.title}
                            placeholder="Add event title"
                            onChange={(event) =>
                                updateEventDraft('title', event.target.value)
                            }
                        />
                    </Form.Field>

                    <Form.Field
                        htmlFor="calendar-event-calendar"
                        label="Calendar"
                    >
                        <Select
                            inputId="calendar-event-calendar"
                            options={calendars.map((calendar) => ({
                                value: calendar.id,
                                label: calendar.label,
                            }))}
                            value={
                                calendars
                                    .map((calendar) => ({
                                        value: calendar.id,
                                        label: calendar.label,
                                    }))
                                    .find(
                                        (option) =>
                                            option.value ===
                                            eventDraft.calendarId,
                                    )
                            }
                            customInputDisplay={(selected) => {
                                const calendar = calendars.find(
                                    (item) => item.id === selected?.value,
                                )

                                return calendar ? (
                                    <span className="flex items-center gap-2">
                                        <span
                                            aria-hidden="true"
                                            className={`size-2 rounded-full ${swatchClass[calendar.color]}`}
                                        />
                                        <span>{calendar.label}</span>
                                    </span>
                                ) : null
                            }}
                            customOption={({ option, selected, CheckIcon }) => {
                                const calendar = calendars.find(
                                    (item) => item.id === option.value,
                                )

                                return (
                                    <span className="flex w-full items-center justify-between gap-2">
                                        <span className="flex min-w-0 items-center gap-2">
                                            {calendar && (
                                                <span
                                                    aria-hidden="true"
                                                    className={`size-2 shrink-0 rounded-full ${swatchClass[calendar.color]}`}
                                                />
                                            )}
                                            <span className="truncate">
                                                {option.label}
                                            </span>
                                        </span>
                                        {selected && CheckIcon}
                                    </span>
                                )
                            }}
                            onChange={(selected) => {
                                if (selected && 'value' in selected) {
                                    updateEventDraft(
                                        'calendarId',
                                        selected.value,
                                    )
                                }
                            }}
                        />
                    </Form.Field>

                    <div className="flex items-center justify-between mb-4">
                        <span className="text-sm font-medium text-foreground">
                            All day
                        </span>
                        <Switcher
                            aria-label="All day"
                            checked={eventDraft.allDay}
                            onChange={(checked) =>
                                updateEventDraft('allDay', checked)
                            }
                        />
                    </div>

                    <Form.Field
                        htmlFor="calendar-event-start-date"
                        label="Starts"
                    >
                        <DatePicker
                            id="calendar-event-start-date"
                            inputFormat="MMMM D, YYYY"
                            value={eventDraft.startDate}
                            clearable={false}
                            onChange={(date) => {
                                if (date) {
                                    updateEventDraft('startDate', date)
                                }
                            }}
                        />
                    </Form.Field>
                    <Form.Field
                        htmlFor="calendar-event-end-date"
                        label="Ends"
                    >
                        <DatePicker
                            id="calendar-event-end-date"
                            inputFormat="MMMM D, YYYY"
                            value={eventDraft.endDate}
                            clearable={false}
                            onChange={(date) => {
                                if (date) {
                                    updateEventDraft('endDate', date)
                                }
                            }}
                        />
                    </Form.Field>

                    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                        <Form.Field
                            htmlFor="calendar-event-start-time"
                            label="From"
                        >
                        <TimeInput
                            id="calendar-event-start-time"
                            value={eventDraft.startTime}
                            format="12"
                            clearable={false}
                            disabled={eventDraft.allDay}
                                onChange={(time) => {
                                    if (time) {
                                        updateEventDraft('startTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                        <Form.Field
                            htmlFor="calendar-event-end-time"
                            label="To"
                        >
                        <TimeInput
                            id="calendar-event-end-time"
                            value={eventDraft.endTime}
                            format="12"
                            clearable={false}
                            disabled={eventDraft.allDay}
                                onChange={(time) => {
                                    if (time) {
                                        updateEventDraft('endTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                    </div>

                    <Form.Field
                        htmlFor="calendar-event-description"
                        label="Description"
                    >
                        <Input
                            id="calendar-event-description"
                            textArea
                            rows={4}
                            value={eventDraft.description}
                            placeholder="Add notes or an agenda"
                            onChange={(event) =>
                                updateEventDraft(
                                    'description',
                                    event.target.value,
                                )
                            }
                        />
                    </Form.Field>
                </Form>
            </Drawer>
        </section>
    )
}

Calendar 06

Preview
npx nateui@latest add CalendarWeekSchedule
Dark
import { useState } from 'react'
import {
    PiArrowUpRight,
    PiCalendarBlank,
    PiCaretLeft,
    PiCaretRight,
    PiClock,
    PiEnvelopeSimple,
    PiMagnifyingGlass,
    PiMapPin,
    PiPhone,
    PiVideoCamera,
} from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import FullCalendar from '@/components/composites/FullCalendar'
import Input from '@/components/ui/Input'
import Popover from '@/components/ui/Popover'
import Segment from '@/components/ui/Segment'
import Select from '@/components/ui/Select'
import Tag from '@/components/ui/Tag'
import classNames from '@/utils/classNames'
import type { CalendarView } from '@/components/composites/FullCalendar'

const headingId = 'calendar-week-schedule-heading'
const assetBase = 'https://statics.nateui.com/img'

const legend = [
    { id: 'onsite', label: 'Onsite Interview', dotClass: 'bg-palette-blue' },
    { id: 'remote', label: 'Remote Interview', dotClass: 'bg-palette-purple' },
]

const tintClass: Record<string, string> = {
    blue: 'bg-palette-blue-soft/60 hover:bg-palette-blue-soft text-palette-blue-soft-foreground border-palette-blue-soft',
    purple: 'bg-palette-purple-soft/60 hover:bg-palette-purple-soft text-palette-purple-soft-foreground border-palette-purple-soft',
}

const modeLabel: Record<string, string> = {
    onsite: 'Onsite',
    remote: 'Remote',
}

const statusOptions = [
    { label: 'All Status', value: 'all' },
    { label: 'Confirmed', value: 'Confirmed' },
    { label: 'Pending', value: 'Pending' },
]

function addDays(date: Date, amount: number) {
    const result = new Date(date)
    result.setDate(result.getDate() + amount)
    return result
}

function getWeekStart(date: Date) {
    const start = new Date(date)
    start.setHours(0, 0, 0, 0)
    start.setDate(start.getDate() - start.getDay())
    return start
}

function toLocalIso(date: Date) {
    const pad = (value: number) => String(value).padStart(2, '0')
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:00`
}

function eventTime(weekStart: Date, dayOffset: number, hour: number, minute: number) {
    const date = addDays(weekStart, dayOffset)
    date.setHours(hour, minute, 0, 0)
    return toLocalIso(date)
}

const today = new Date()
const currentWeekStart = getWeekStart(today)
const previousWeekStart = addDays(currentWeekStart, -7)
const nextWeekStart = addDays(currentWeekStart, 7)

function timeAround(offsetMinutes: number, durationMinutes: number) {
    const start = new Date(today)
    start.setMinutes(start.getMinutes() + offsetMinutes)
    const end = new Date(start)
    end.setMinutes(end.getMinutes() + durationMinutes)
    return {
        start: [start.getHours(), start.getMinutes()] as [number, number],
        end: [end.getHours(), end.getMinutes()] as [number, number],
    }
}

type EventSeed = {
    id: number
    type: string
    color: string
    title: string
    description: string
    weekStart: Date
    dayOffset: number
    start: [number, number]
    end: [number, number]
}

function buildEvent(seed: EventSeed) {
    return {
        id: seed.id,
        type: seed.type,
        color: seed.color,
        title: seed.title,
        description: seed.description,
        startDate: eventTime(seed.weekStart, seed.dayOffset, seed.start[0], seed.start[1]),
        endDate: eventTime(seed.weekStart, seed.dayOffset, seed.end[0], seed.end[1]),
    }
}

const initialEvents = [
    buildEvent({ id: 1, type: 'remote', color: 'purple', title: 'Elena Ruiz', description: 'Phone Screen · Product Designer', weekStart: currentWeekStart, dayOffset: 1, ...timeAround(140, 40) }),
    buildEvent({ id: 2, type: 'remote', color: 'purple', title: 'Nadia Farouk', description: 'Hiring Manager Chat · Product Manager', weekStart: currentWeekStart, dayOffset: 2, ...timeAround(-140, 45) }),
    buildEvent({ id: 3, type: 'onsite', color: 'blue', title: 'Connor Blake', description: 'Technical Round · DevOps Engineer', weekStart: currentWeekStart, dayOffset: 3, ...timeAround(20, 45) }),
    buildEvent({ id: 4, type: 'remote', color: 'purple', title: 'Renee Dupont', description: 'Final Round · Marketing Lead', weekStart: currentWeekStart, dayOffset: 4, ...timeAround(-80, 45) }),
    buildEvent({ id: 5, type: 'onsite', color: 'blue', title: 'Marcus Webb', description: 'Panel Interview · Sales Engineer', weekStart: currentWeekStart, dayOffset: 5, ...timeAround(-110, 40) }),
    buildEvent({ id: 6, type: 'remote', color: 'purple', title: 'Samuel Osei', description: 'Final Round · Data Analyst', weekStart: currentWeekStart, dayOffset: 6, ...timeAround(60, 35) }),
    buildEvent({ id: 7, type: 'remote', color: 'purple', title: 'Tobias Kane', description: 'Technical Round · Backend Engineer', weekStart: currentWeekStart, dayOffset: 0, ...timeAround(90, 45) }),
    buildEvent({ id: 8, type: 'onsite', color: 'blue', title: 'Priya Nair', description: 'Intro Call · Frontend Engineer', weekStart: currentWeekStart, dayOffset: 0, ...timeAround(-20, 45) }),
    buildEvent({ id: 9, type: 'remote', color: 'purple', title: 'Yuki Tanaka', description: 'Phone Screen · UX Researcher', weekStart: previousWeekStart, dayOffset: 2, start: [5, 30], end: [6, 15] }),
    buildEvent({ id: 10, type: 'remote', color: 'purple', title: 'Aisha Bello', description: 'Technical Round · Content Strategist', weekStart: previousWeekStart, dayOffset: 4, start: [12, 0], end: [12, 45] }),
    buildEvent({ id: 11, type: 'onsite', color: 'blue', title: 'Elena Ruiz', description: 'Final Round · Product Designer', weekStart: previousWeekStart, dayOffset: 5, start: [17, 0], end: [17, 45] }),
    buildEvent({ id: 12, type: 'remote', color: 'purple', title: 'Tobias Kane', description: 'Panel Interview · Backend Engineer', weekStart: nextWeekStart, dayOffset: 1, start: [19, 0], end: [19, 45] }),
    buildEvent({ id: 13, type: 'onsite', color: 'blue', title: 'Connor Blake', description: 'Final Round · DevOps Engineer', weekStart: nextWeekStart, dayOffset: 3, start: [12, 30], end: [13, 15] }),
    buildEvent({ id: 14, type: 'remote', color: 'purple', title: 'Nadia Farouk', description: 'Final Round · Product Manager', weekStart: nextWeekStart, dayOffset: 5, start: [16, 30], end: [17, 15] }),
    buildEvent({ id: 15, type: 'onsite', color: 'blue', title: 'Samuel Osei', description: 'Culture Fit · Data Analyst', weekStart: currentWeekStart, dayOffset: 4, ...timeAround(70, 30) }),
]

const candidateDetails: Record<
    number,
    { avatar: string; phone: string; email: string; location: string; status: string }
> = {
    1: { avatar: `${assetBase}/avatars/thumb-1.jpg`, phone: '+1 415 555 0132', email: 'elena.ruiz@examplemail.com', location: 'Austin, TX', status: 'Confirmed' },
    2: { avatar: `${assetBase}/avatars/thumb-5.jpg`, phone: '+1 213 555 0187', email: 'nadia.farouk@examplemail.com', location: 'Video call', status: 'Confirmed' },
    3: { avatar: `${assetBase}/avatars/thumb-6.jpg`, phone: '+1 720 555 0119', email: 'connor.blake@examplemail.com', location: 'Denver, CO', status: 'Pending' },
    4: { avatar: `${assetBase}/avatars/thumb-8.jpg`, phone: '+1 617 555 0152', email: 'renee.dupont@examplemail.com', location: 'Video call', status: 'Confirmed' },
    5: { avatar: `${assetBase}/avatars/thumb-9.jpg`, phone: '+1 469 555 0176', email: 'marcus.webb@examplemail.com', location: 'Austin, TX', status: 'Confirmed' },
    6: { avatar: `${assetBase}/avatars/thumb-4.jpg`, phone: '+1 312 555 0143', email: 'samuel.osei@examplemail.com', location: 'Chicago, IL', status: 'Confirmed' },
    7: { avatar: `${assetBase}/avatars/thumb-2.jpg`, phone: '+1 512 555 0198', email: 'tobias.kane@examplemail.com', location: 'Video call', status: 'Confirmed' },
    8: { avatar: `${assetBase}/avatars/thumb-3.jpg`, phone: '+1 646 555 0176', email: 'priya.nair@examplemail.com', location: 'Video call', status: 'Pending' },
    9: { avatar: `${assetBase}/avatars/thumb-7.jpg`, phone: '+1 206 555 0164', email: 'yuki.tanaka@examplemail.com', location: 'Seattle, WA', status: 'Confirmed' },
    10: { avatar: `${assetBase}/avatars/thumb-10.jpg`, phone: '+1 305 555 0128', email: 'aisha.bello@examplemail.com', location: 'Video call', status: 'Pending' },
    11: { avatar: `${assetBase}/avatars/thumb-1.jpg`, phone: '+1 415 555 0132', email: 'elena.ruiz@examplemail.com', location: 'Austin, TX', status: 'Confirmed' },
    12: { avatar: `${assetBase}/avatars/thumb-2.jpg`, phone: '+1 512 555 0198', email: 'tobias.kane@examplemail.com', location: 'Toronto, ON', status: 'Pending' },
    13: { avatar: `${assetBase}/avatars/thumb-6.jpg`, phone: '+1 720 555 0119', email: 'connor.blake@examplemail.com', location: 'Denver, CO', status: 'Confirmed' },
    14: { avatar: `${assetBase}/avatars/thumb-5.jpg`, phone: '+1 213 555 0187', email: 'nadia.farouk@examplemail.com', location: 'Video call', status: 'Confirmed' },
    15: { avatar: `${assetBase}/avatars/thumb-4.jpg`, phone: '+1 312 555 0143', email: 'samuel.osei@examplemail.com', location: 'Chicago, IL', status: 'Pending' },
}

function formatTime(iso: string) {
    return new Date(iso).toLocaleTimeString('en-US', {
        hour: 'numeric',
        minute: '2-digit',
    })
}

function formatDateTime(iso: string) {
    const date = new Date(iso)
    return `${date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' })} · ${formatTime(iso)}`
}

type EventContentProps = {
    id: number
    title: string
    color: string
    type: string
    description: string
    startDate: string
    endDate: string
    isOpen: boolean
    onOpenChange: (open: boolean) => void
    view: 'week' | 'month'
}

function buildEventContent({
    id,
    title,
    color,
    type,
    description,
    startDate,
    endDate,
    isOpen,
    view,
    onOpenChange,
}: EventContentProps) {
    const durationMinutes = Math.round(
        (new Date(endDate).getTime() - new Date(startDate).getTime()) / 60000,
    )
    const isCompact = durationMinutes < 35
    const details = candidateDetails[id]
    const tint = tintClass[color] ?? tintClass.blue

    return {
        className: classNames(tint, 'border-transparent p-0'),
        content: (
            <Popover
                open={isOpen}
                onOpenChange={onOpenChange}
                trigger="click"
                placement="right-start"
                className="p-0"
                renderTrigger={
                    <div
                        className={classNames(
                            'flex h-full w-full min-w-0 outline-none',
                            view === 'month'
                                ? 'items-center gap-1 px-2'
                                : 'items-start gap-2 p-2',
                        )}
                    >
                        <Avatar
                            src={details.avatar}
                            alt=""
                            size={view === 'month' ? 20 : 16}
                            className="shrink-0"
                        />
                        <div className="min-w-0">
                            <p className="truncate font-medium">{title}</p>
                            {view === 'week' && !isCompact && (
                                <p className="truncate">
                                    {formatTime(startDate)} - {formatTime(endDate)}
                                </p>
                            )}
                        </div>
                    </div>
                }
            >
                <div className="p-4">
                    <div className="flex items-center gap-2">
                        <Avatar
                            src={details.avatar}
                            alt=""
                        />
                        <div className="min-w-0">
                            <div className="truncate font-semibold text-popover-foreground">
                                {title}
                            </div>
                            <div className="truncate text-xs font-medium text-muted-foreground">
                                {description}
                            </div>
                        </div>
                    </div>

                    <div className="space-y-4 mt-4">
                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiVideoCamera aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            {modeLabel[type] ?? type}
                        </div>

                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiClock aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            {durationMinutes} min
                        </div>

                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiPhone aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            <span className="truncate">{details.phone}</span>
                        </div>

                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiEnvelopeSimple aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            <span className="truncate">{details.email}</span>
                        </div>

                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiCalendarBlank aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            {formatDateTime(startDate)}
                        </div>

                        <div className="flex items-center gap-2 text-popover-foreground">
                            <PiMapPin aria-hidden="true" className="shrink-0 text-base text-muted-foreground" />
                            {details.location}
                        </div>
                    </div>

                    <Button className="mt-4 w-full" icon={<PiArrowUpRight aria-hidden="true" />}>
                        View details
                    </Button>
                </div>
            </Popover>
        ),
    }
}

export default function CalendarWeekSchedule() {
    const [events, setEvents] = useState(initialEvents)
    const [searchQuery, setSearchQuery] = useState('')
    const [statusFilter, setStatusFilter] = useState('all')
    const [openEventId, setOpenEventId] = useState<number | null>(null)

    const query = searchQuery.trim().toLowerCase()

    const filteredEvents = events.filter((event) => {
        const matchesQuery = !query || event.title.toLowerCase().includes(query)
        const matchesStatus =
            statusFilter === 'all' || candidateDetails[event.id].status === statusFilter
        return matchesQuery && matchesStatus
    })

    return (
        <section aria-labelledby={headingId}>
            <h5 id={headingId} className="sr-only">
                Interview week schedule
            </h5>
            <div>
                <div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-3">
                    <div>
                        <span className="text-lg font-semibold">
                            {filteredEvents.length}
                        </span>{' '}
                        <span className="text-muted-foreground">Interviews</span>
                    </div>
                    <div className="flex flex-wrap items-center gap-2">
                        <Input
                            prefix={
                                <PiMagnifyingGlass
                                    aria-hidden="true"
                                    className="text-base text-muted-foreground"
                                />
                            }
                            placeholder="Search candidates"
                            value={searchQuery}
                            onChange={(event) =>
                                setSearchQuery(event.target.value)
                            }
                            className="w-48"
                        />
                        <Select
                            className="w-36"
                            options={statusOptions}
                            value={statusOptions.find(
                                (option) => option.value === statusFilter,
                            )}
                            onChange={(selected) => {
                                if (selected && 'value' in selected) {
                                    setStatusFilter(selected.value as string)
                                }
                            }}
                        />
                    </div>
                </div>
                <div className="w-full h-225 xl:h-screen ">
                    <FullCalendar
                        fillHeight
                        events={filteredEvents}
                        view="week"
                        onChange={(_filteredEvents, updatedEvent) => {
                            setEvents((prev) =>
                                prev.map((event) =>
                                    event.id === updatedEvent.id
                                        ? updatedEvent
                                        : event,
                                ),
                            )
                        }}
                        renderEvent={({ event, view }) =>
                            buildEventContent({
                                id: event.id,
                                title: event.title,
                                color: event.color,
                                type: event.type,
                                description: event.description,
                                startDate: event.startDate,
                                endDate: event.endDate,
                                isOpen: openEventId === event.id,
                                view: view as 'week' | 'month',
                                onOpenChange: (open) =>
                                    setOpenEventId(open ? event.id : null),
                            })
                        }
                        renderHeaderStart={({
                            handlePrevious,
                            selectedDate,
                            handleNext,
                        }) => (
                            <div className="flex flex-wrap items-center gap-4">
                                <div className="flex items-center gap-2">
                                    <Button
                                        aria-label="Previous period"
                                        icon={<PiCaretLeft aria-hidden="true" />}
                                        variant="ghost"
                                        size="sm"
                                        onClick={handlePrevious}
                                    />
                                    <Button
                                        aria-label="Next period"
                                        icon={<PiCaretRight aria-hidden="true" />}
                                        variant="ghost"
                                        size="sm"
                                        onClick={handleNext}
                                    />
                                    <h5 className="ml-1">
                                        {selectedDate.toLocaleDateString('en-US', {
                                            month: 'long',
                                            year: 'numeric',
                                        })}
                                    </h5>
                                </div>
                                <div className="flex items-center gap-4">
                                    {legend.map((item) => (
                                        <Tag
                                            key={item.id}
                                            className="gap-1"
                                        >
                                            <span
                                                aria-hidden="true"
                                                className={classNames(
                                                    'size-2 shrink-0 rounded-full',
                                                    item.dotClass,
                                                )}
                                            />
                                            {item.label}
                                        </Tag>
                                    ))}
                                </div>
                            </div>
                        )}
                        renderHeaderEnd={({
                            view,
                            setView,
                        }) => (
                            <div className="flex flex-wrap items-center gap-2">
                                <Segment
                                    value={view}
                                    onChange={(value) =>
                                        setView(value as CalendarView)
                                    }
                                >
                                    <Segment.Item value="week">Week</Segment.Item>
                                    <Segment.Item value="month">Month</Segment.Item>
                                </Segment>
                            </div>
                        )}
                    />
                </div>
            </div>
        </section>
    )
}

Calendar 07

Preview
npx nateui@latest add CalendarMonthSideRail
Dark
import { useEffect, useRef, useState } from 'react'
import {
    PiCaretLeft,
    PiCaretRight,
    PiPlus
} from 'react-icons/pi'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import Checkbox from '@/components/ui/Checkbox'
import DatePicker from '@/components/ui/DatePicker'
import Drawer from '@/components/ui/Drawer'
import Form from '@/components/ui/Form'
import InputGroup from '@/components/ui/InputGroup'
import FullCalendar from '@/components/composites/FullCalendar'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import TimeInput from '@/components/ui/TimeInput'
import classNames from '@/utils/classNames'
import type { RefObject } from 'react'

const headingId = 'calendar-month-side-rail-heading'
const eventFormId = 'calendar-month-side-rail-event-form'

const calendarSources = [
    { id: 'personal', label: 'Personal', color: 'blue', dotClass: 'bg-palette-blue' },
    { id: 'work', label: 'Work', color: 'green', dotClass: 'bg-palette-emerald' },
    { id: 'clients', label: 'Clients', color: 'orange', dotClass: 'bg-palette-orange' },
    { id: 'projects', label: 'Projects', color: 'purple', dotClass: 'bg-palette-purple' },
] as const

const tintClass: Record<string, string> = {
    blue: 'bg-palette-blue-soft text-palette-blue',
    green: 'bg-palette-emerald-soft text-palette-emerald',
    orange: 'bg-palette-orange-soft text-palette-orange',
    purple: 'bg-palette-purple-soft text-palette-purple',
}

function toLocalIso(date: Date) {
    const pad = (value: number) => String(value).padStart(2, '0')
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:00`
}

const monthAnchor = new Date()

function monthEventTime(day: number, hour: number, minute: number) {
    const date = new Date(
        monthAnchor.getFullYear(),
        monthAnchor.getMonth(),
        day,
        hour,
        minute,
        0,
        0,
    )
    return toLocalIso(date)
}

function dateKey(date: Date) {
    return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
}

function isSameDay(a: Date, b: Date) {
    return dateKey(a) === dateKey(b)
}

type EventSeed = {
    id: number
    sourceId: string
    title: string
    description: string
    day: number
    start: [number, number]
    end: [number, number]
}

function buildEvent(seed: EventSeed) {
    const source = calendarSources.find((item) => item.id === seed.sourceId)
    const color: string = source ? source.color : 'gray'
    return {
        id: seed.id,
        type: seed.sourceId,
        color,
        title: seed.title,
        description: seed.description,
        startDate: monthEventTime(seed.day, seed.start[0], seed.start[1]),
        endDate: monthEventTime(seed.day, seed.end[0], seed.end[1]),
    }
}

// Irregular on purpose: clusters, empty stretches, varied durations/times,
// and day 14 with one more event than the grid's visible capacity so the
// month view's "+N more" affordance actually appears.
const initialEvents = [
    buildEvent({ id: 1, sourceId: 'personal', title: 'Design review', description: 'Walkthrough of the latest screens with feedback.', day: 1, start: [9, 15], end: [10, 0] }),
    buildEvent({ id: 2, sourceId: 'work', title: 'Client call', description: 'Weekly check-in with a client contact.', day: 3, start: [8, 30], end: [9, 0] }),
    buildEvent({ id: 4, sourceId: 'projects', title: 'Release checklist', description: 'Confirm the checklist before the release window.', day: 3, start: [15, 45], end: [16, 30] }),
    buildEvent({ id: 5, sourceId: 'personal', title: 'Quarterly planning', description: 'Set scope and priorities for the quarter.', day: 7, start: [10, 0], end: [12, 0] }),
    buildEvent({ id: 6, sourceId: 'work', title: 'Standup', description: 'Daily standup with the immediate team.', day: 8, start: [9, 0], end: [9, 15] }),
    buildEvent({ id: 7, sourceId: 'clients', title: 'Onboarding session', description: 'Walk a new teammate through the current setup.', day: 12, start: [13, 30], end: [14, 0] }),
    buildEvent({ id: 8, sourceId: 'projects', title: 'Interview slot', description: 'Screening interview for an open role.', day: 12, start: [16, 0], end: [16, 45] }),
    buildEvent({ id: 9, sourceId: 'personal', title: 'Vendor sync', description: 'Status update call with a vendor contact.', day: 14, start: [8, 0], end: [8, 30] }),
    buildEvent({ id: 10, sourceId: 'work', title: 'Content draft', description: 'First pass review on a draft in progress.', day: 14, start: [9, 0], end: [9, 20] }),
    buildEvent({ id: 11, sourceId: 'clients', title: 'Support rotation', description: 'Start of the weekly support rotation shift.', day: 14, start: [10, 0], end: [10, 30] }),
    buildEvent({ id: 12, sourceId: 'projects', title: 'Team lunch', description: 'Informal lunch with the immediate team.', day: 14, start: [12, 0], end: [12, 30] }),
    buildEvent({ id: 15, sourceId: 'clients', title: 'Roadmap sync', description: 'Align on roadmap priorities with the client.', day: 15, start: [9, 30], end: [10, 0] }),
    buildEvent({ id: 16, sourceId: 'personal', title: 'Portfolio review', description: 'Review current project portfolio status.', day: 19, start: [8, 45], end: [9, 30] }),
    buildEvent({ id: 18, sourceId: 'projects', title: 'Contract review', description: 'Review terms before countersigning.', day: 19, start: [14, 0], end: [14, 30] }),
    buildEvent({ id: 19, sourceId: 'clients', title: 'Travel booking', description: 'Confirm travel arrangements for the client visit.', day: 22, start: [7, 30], end: [8, 0] }),
    buildEvent({ id: 20, sourceId: 'personal', title: 'Expense review', description: 'Review and approve outstanding expenses.', day: 23, start: [18, 30], end: [19, 0] }),
    buildEvent({ id: 21, sourceId: 'work', title: 'Client workshop', description: 'Facilitate a working session with a client.', day: 28, start: [10, 15], end: [10, 45] }),
    buildEvent({ id: 22, sourceId: 'projects', title: 'Workshop prep', description: 'Prepare materials for the client workshop.', day: 28, start: [14, 15], end: [15, 0] }),
]

type EventDraft = {
    title: string
    sourceId: string
    date: Date
    startTime: Date
    endTime: Date
    description: string
}

function defaultDraftDate() {
    const today = new Date()
    today.setHours(0, 0, 0, 0)
    return today
}

function createDraft(date: Date = defaultDraftDate()): EventDraft {
    const start = new Date(date)
    start.setHours(9, 0, 0, 0)
    const end = new Date(start)
    end.setMinutes(end.getMinutes() + 30)
    return {
        title: '',
        sourceId: calendarSources[0].id,
        date: new Date(date),
        startTime: start,
        endTime: end,
        description: '',
    }
}

function createDraftFromEvent(event: {
    type: string
    title: string
    description: string
    startDate: string
    endDate: string
}): EventDraft {
    const startTime = new Date(event.startDate)
    const endTime = new Date(event.endDate)
    const date = new Date(startTime)
    date.setHours(0, 0, 0, 0)
    return {
        title: event.title,
        sourceId: event.type,
        date,
        startTime,
        endTime,
        description: event.description,
    }
}

function combineDateAndTime(date: Date, time: Date) {
    const combined = new Date(date)
    combined.setHours(time.getHours(), time.getMinutes(), 0, 0)
    return combined
}

function GridDateSync({
    bind,
    selectedDate,
    onDateChange,
    setSelectedDate,
}: {
    bind: RefObject<((date: Date) => void) | null>
    selectedDate: Date
    onDateChange: (date: Date) => void
    setSelectedDate: (date: Date) => void
}) {
    useEffect(() => {
        bind.current = setSelectedDate
        return () => {
            bind.current = null
        }
    }, [bind, setSelectedDate])

    useEffect(() => {
        onDateChange(selectedDate)
    }, [selectedDate, onDateChange])

    return null
}

export default function CalendarMonthSideRail() {
    const [events, setEvents] = useState(initialEvents)
    const [enabledSourceIds, setEnabledSourceIds] = useState<Set<string>>(
        () => new Set(calendarSources.map((source) => source.id)),
    )
    const [activeDate, setActiveDate] = useState(() => new Date())
    const setGridDate = useRef<((date: Date) => void) | null>(null)

    const [isDrawerOpen, setDrawerOpen] = useState(false)
    const [editingEventId, setEditingEventId] = useState<number | null>(null)
    const [draft, setDraft] = useState<EventDraft>(() => createDraft())
    const [formError, setFormError] = useState<string | null>(null)

    const filteredEvents = events.filter((event) =>
        enabledSourceIds.has(event.type),
    )

    const sourceIdByDay = new Map<string, string>()
    filteredEvents.forEach((event) => {
        const key = dateKey(new Date(event.startDate))
        if (!sourceIdByDay.has(key)) {
            sourceIdByDay.set(key, event.type)
        }
    })

    const toggleSource = (id: string, checked: boolean) => {
        setEnabledSourceIds((prev) => {
            const next = new Set(prev)
            if (checked) {
                next.add(id)
            } else {
                next.delete(id)
            }
            return next
        })
    }

    const openCreateDrawer = (date?: Date) => {
        setDraft(createDraft(date))
        setEditingEventId(null)
        setFormError(null)
        setDrawerOpen(true)
    }

    const openEditDrawer = (event: {
        id: number
        type: string
        title: string
        description: string
        startDate: string
        endDate: string
    }) => {
        setDraft(createDraftFromEvent(event))
        setEditingEventId(event.id)
        setFormError(null)
        setDrawerOpen(true)
    }

    const closeDrawer = () => {
        setDrawerOpen(false)
        setEditingEventId(null)
        setFormError(null)
    }

    const updateDraft = <Key extends keyof EventDraft>(
        key: Key,
        value: EventDraft[Key],
    ) => {
        setDraft((prev) => ({ ...prev, [key]: value }))
        setFormError(null)
    }

    const handleSubmitEvent = () => {
        const title = draft.title.trim()
        if (!title) {
            setFormError('Add a title before saving.')
            return
        }

        const startDate = combineDateAndTime(draft.date, draft.startTime)
        const endDate = combineDateAndTime(draft.date, draft.endTime)
        if (endDate <= startDate) {
            setFormError('End time must be after start time.')
            return
        }

        const source =
            calendarSources.find((item) => item.id === draft.sourceId) ??
            calendarSources[0]
        const description = draft.description.trim()

        if (editingEventId !== null) {
            setEvents((prev) =>
                prev.map((event) =>
                    event.id === editingEventId
                        ? {
                              ...event,
                              type: source.id,
                              color: source.color,
                              title,
                              description,
                              startDate: toLocalIso(startDate),
                              endDate: toLocalIso(endDate),
                          }
                        : event,
                ),
            )
        } else {
            const nextId =
                Math.max(0, ...events.map((event) => event.id)) + 1
            setEvents((prev) => [
                ...prev,
                {
                    id: nextId,
                    type: source.id,
                    color: source.color,
                    title,
                    description,
                    startDate: toLocalIso(startDate),
                    endDate: toLocalIso(endDate),
                },
            ])
        }

        setEnabledSourceIds((prev) => {
            const next = new Set(prev)
            next.add(source.id)
            return next
        })
        closeDrawer()
    }

    const renderMiniDay = (date: Date) => {
        const key = dateKey(date)
        const sourceId = sourceIdByDay.get(key)
        const source = sourceId
            ? calendarSources.find((item) => item.id === sourceId)
            : undefined
        const isSelected = isSameDay(date, activeDate)

        return (
            <span className="relative flex h-full w-full items-center justify-center">
                <span>{date.getDate()}</span>
                {source && (
                    <span
                        aria-hidden="true"
                        className={classNames(
                            'absolute inset-x-0 bottom-0.5 mx-auto h-1 w-1 rounded-full',
                            isSelected ? 'bg-primary-foreground' : source.dotClass,
                        )}
                    />
                )}
            </span>
        )
    }

    return (
        <section
            aria-labelledby={headingId}
            className="grid min-h-0 grid-cols-1 lg:grid-cols-[minmax(280px,290px)_minmax(0,1fr)] h-225 xl:h-screen"
        >
            <aside className="order-2 flex min-h-0 min-w-0 flex-col gap-4 overflow-y-auto border-t px-4 py-4 lg:order-1 lg:border-t-0 lg:border-r lg:px-4">
                <Calendar
                    firstDayOfWeek="sunday"
                    value={activeDate}
                    onChange={(date) => {
                        if (!date) return
                        setActiveDate(date)
                        setGridDate.current?.(date)
                    }}
                    renderDay={renderMiniDay}
                />

                <div>
                    <h6 className="text-base font-semibold">
                        Calendars
                    </h6>
                    <div className="mt-2 flex flex-col">
                        {calendarSources.map((source) => (
                            <Checkbox
                                key={source.id}
                                checked={enabledSourceIds.has(source.id)}
                                onChange={(checked) =>
                                    toggleSource(source.id, checked)
                                }
                                className="flex-row-reverse justify-between p-2 rounded-md hover:bg-muted"
                            >
                                <span className="flex items-center gap-2">
                                    <span
                                        aria-hidden="true"
                                        className={classNames(
                                            'size-2 shrink-0 rounded-full',
                                            source.dotClass,
                                        )}
                                    />
                                    {source.label}
                                </span>
                            </Checkbox>
                        ))}
                    </div>
                </div>
            </aside>

            <div className="order-1 flex min-h-0 min-w-0 flex-col lg:order-2">
                <FullCalendar
                    fillHeight
                    view="month"
                    events={filteredEvents}
                    onCellClick={openCreateDrawer}
                    onEventClick={openEditDrawer}
                    onChange={(_filteredEvents, updatedEvent) => {
                        setEvents((prev) =>
                            prev.map((event) =>
                                event.id === updatedEvent.id
                                    ? updatedEvent
                                    : event,
                            ),
                        )
                    }}
                    renderEvent={({ event }) => {
                        const tint = tintClass[event.color] ?? tintClass.blue
                        const source = calendarSources.find(
                            (item) => item.id === event.type,
                        )
                        return {
                            className: 'bg-card border',
                            content: (
                                <div className="flex min-w-0 items-center gap-1.5 truncate">
                                    <span
                                        aria-hidden="true"
                                        className={classNames(
                                            'size-1.5 shrink-0 rounded-full',
                                            source?.dotClass ?? 'bg-palette-gray',
                                        )}
                                    />
                                    <span className="font-medium">
                                        {event.title}
                                    </span>
                                    <span className="shrink-0 text-muted-foreground">
                                        {new Date(event.startDate).toLocaleTimeString('en-US', {
                                            hour: 'numeric',
                                            minute: '2-digit',
                                        })}
                                    </span>
                                </div>
                            ),
                        }
                    }}
                    renderHeaderStart={({
                        selectedDate,
                        setSelectedDate,
                    }) => (
                        <>
                            <GridDateSync
                                bind={setGridDate}
                                selectedDate={selectedDate}
                                setSelectedDate={setSelectedDate}
                                onDateChange={setActiveDate}
                            />
                            <div className="flex items-center gap-3">
                                <div className="flex items-center gap-1">
                                    
                                    
                                </div>
                                <h5 id={headingId} className="text-lg font-semibold">
                                    {selectedDate.toLocaleDateString('en-US', {
                                        month: 'long',
                                        year: 'numeric',
                                    })}
                                </h5>
                            </div>
                        </>
                    )}
                    renderHeaderEnd={({ 
                        setSelectedDate,
                        handlePrevious,
                        handleNext,
                    }) => (
                        <div className="flex items-center gap-2">
                            <InputGroup>
                                <Button
                                    aria-label="Previous month"
                                    icon={<PiCaretLeft aria-hidden="true" />}
                                    size="sm"
                                    onClick={handlePrevious}
                                />
                                <Button
                                    variant="default"
                                    size="sm"
                                    onClick={() => setSelectedDate(new Date())}
                                >
                                    Today
                                </Button>
                                <Button
                                    aria-label="Next month"
                                    icon={<PiCaretRight aria-hidden="true" />}
                                    size="sm"
                                    onClick={handleNext}
                                />
                            </InputGroup>
                            <Button
                                variant="solid"
                                size="sm"
                                icon={<PiPlus aria-hidden="true" />}
                                onClick={() => openCreateDrawer()}
                            >
                                Add event
                            </Button>
                        </div>
                    )}
                />
            </div>

            <Drawer
                isOpen={isDrawerOpen}
                placement="right"
                title={editingEventId !== null ? 'Edit event' : 'New event'}
                onClose={closeDrawer}
                footerClass="flex justify-end gap-2"
                footer={
                    <>
                        <Button type="button" onClick={closeDrawer}>
                            Cancel
                        </Button>
                        <Button
                            variant="solid"
                            form={eventFormId}
                            type="submit"
                        >
                            {editingEventId !== null ? 'Save changes' : 'Create event'}
                        </Button>
                    </>
                }
            >
                <Form
                    id={eventFormId}
                    className="space-y-4"
                    onSubmit={(event) => {
                        event.preventDefault()
                        handleSubmitEvent()
                    }}
                >
                    {formError && (
                        <p
                            className="rounded-control bg-destructive-soft px-3 py-2 text-sm text-destructive"
                            role="alert"
                        >
                            {formError}
                        </p>
                    )}
                    <Form.Field htmlFor="calendar-month-event-title" label="Title">
                        <Input
                            id="calendar-month-event-title"
                            value={draft.title}
                            placeholder="Add event title"
                            onChange={(event) =>
                                updateDraft('title', event.target.value)
                            }
                        />
                    </Form.Field>

                    <Form.Field htmlFor="calendar-month-event-source" label="Calendar">
                        <Select
                            inputId="calendar-month-event-source"
                            options={calendarSources.map((source) => ({
                                value: source.id,
                                label: source.label,
                            }))}
                            value={calendarSources
                                .map((source) => ({
                                    value: source.id,
                                    label: source.label,
                                }))
                                .find((option) => option.value === draft.sourceId)}
                            customOption={({ option, selected, CheckIcon }) => {
                                const source = calendarSources.find(
                                    (item) => item.id === option.value,
                                )
                                return (
                                    <span className="flex w-full items-center justify-between gap-2">
                                        <span className="flex min-w-0 items-center gap-2">
                                            {source && (
                                                <span
                                                    aria-hidden="true"
                                                    className={classNames(
                                                        'size-2 shrink-0 rounded-full',
                                                        source.dotClass,
                                                    )}
                                                />
                                            )}
                                            <span className="truncate">{option.label}</span>
                                        </span>
                                        {selected && CheckIcon}
                                    </span>
                                )
                            }}
                            onChange={(selected) => {
                                if (selected && 'value' in selected) {
                                    updateDraft('sourceId', selected.value as string)
                                }
                            }}
                        />
                    </Form.Field>

                    <Form.Field htmlFor="calendar-month-event-date" label="Date">
                        <DatePicker
                            id="calendar-month-event-date"
                            inputFormat="MMMM D, YYYY"
                            value={draft.date}
                            clearable={false}
                            onChange={(date) => {
                                if (date) {
                                    updateDraft('date', date)
                                }
                            }}
                        />
                    </Form.Field>

                    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                        <Form.Field htmlFor="calendar-month-event-start" label="From">
                            <TimeInput
                                id="calendar-month-event-start"
                                value={draft.startTime}
                                format="12"
                                clearable={false}
                                onChange={(time) => {
                                    if (time) {
                                        updateDraft('startTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                        <Form.Field htmlFor="calendar-month-event-end" label="To">
                            <TimeInput
                                id="calendar-month-event-end"
                                value={draft.endTime}
                                format="12"
                                clearable={false}
                                onChange={(time) => {
                                    if (time) {
                                        updateDraft('endTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                    </div>

                    <Form.Field htmlFor="calendar-month-event-description" label="Description">
                        <Input
                            id="calendar-month-event-description"
                            textArea
                            rows={4}
                            value={draft.description}
                            placeholder="Add notes or an agenda"
                            onChange={(event) =>
                                updateDraft('description', event.target.value)
                            }
                        />
                    </Form.Field>
                </Form>
            </Drawer>
        </section>
    )
}

Calendar 08

Preview
npx nateui@latest add CalendarDayFocusRail
Dark
import { useEffect, useRef, useState } from 'react'
import {
    PiCheck,
    PiCalendarBlank,
    PiCaretLeft,
    PiCaretRight,
    PiClock,
    PiPencilSimple,
    PiPlus,
    PiTrash,
} from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import DatePicker from '@/components/ui/DatePicker'
import Dialog from '@/components/ui/Dialog'
import Drawer from '@/components/ui/Drawer'
import Form from '@/components/ui/Form'
import EmptyState from '@/components/composites/EmptyState'
import FullCalendar from '@/components/composites/FullCalendar'
import IconFrame from '@/components/composites/IconFrame'
import Input from '@/components/ui/Input'
import Popover from '@/components/ui/Popover'
import InputGroup from '@/components/ui/InputGroup'
import Scroll from '@/components/ui/Scroll'
import Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import TimeInput from '@/components/ui/TimeInput'
import type { RefObject } from 'react'
import type {
    CalendarView,
    FullCalendarEvent,
} from '@/components/composites/FullCalendar'

const headingId = 'calendar-day-focus-rail-heading'
const eventFormId = 'calendar-day-focus-rail-event-form'
const assetBase = 'https://statics.nateui.com/img'

const calendars = [
    { id: 'fieldwork', label: 'Fieldwork', color: 'blue' },
    { id: 'planning', label: 'Planning', color: 'purple' },
    { id: 'operations', label: 'Operations', color: 'green' },
]

const calendarOptions = calendars.map((calendar) => ({
    value: calendar.id,
    label: calendar.label,
}))

const swatchClass: Record<string, string> = {
    blue: 'bg-palette-blue',
    purple: 'bg-palette-purple',
    green: 'bg-palette-emerald',
}

const viewOptions: { label: string; value: CalendarView }[] = [
    { label: 'Month', value: 'month' },
    { label: 'Week', value: 'week' },
    { label: 'Day', value: 'day' },
    { label: 'Agenda', value: 'agenda' },
]

const guestOptions = [
    { id: 'mira', name: 'Mira Chen', avatar: `${assetBase}/avatars/thumb-2.jpg` },
    { id: 'noah', name: 'Noah Patel', avatar: `${assetBase}/avatars/thumb-4.jpg` },
    { id: 'rhea', name: 'Rhea Morgan', avatar: `${assetBase}/avatars/thumb-6.jpg` },
    { id: 'aiko', name: 'Aiko Tan', avatar: `${assetBase}/avatars/thumb-8.jpg` },
    { id: 'jules', name: 'Jules Martin', avatar: `${assetBase}/avatars/thumb-10.jpg` },
]

const initialGuestIds: Record<number, string[]> = {
    1: ['mira', 'noah', 'rhea'],
    2: ['aiko', 'mira'],
    3: ['noah', 'rhea', 'aiko', 'jules'],
    4: ['rhea', 'jules'],
    5: ['mira', 'aiko', 'jules'],
}

const eventTintClass: Record<string, string> = {
    blue: 'bg-palette-blue-soft/60 hover:bg-palette-blue-soft text-palette-blue-soft-foreground border-palette-blue-soft',
    green: 'bg-palette-emerald-soft/60 hover:bg-palette-emerald-soft text-palette-emerald-soft-foreground border-palette-emerald-soft',
    purple: 'bg-palette-purple-soft/60 hover:bg-palette-purple-soft text-palette-purple-soft-foreground border-palette-purple-soft',
}

const startOfToday = () => {
    const date = new Date()
    date.setHours(0, 0, 0, 0)
    return date
}

const todayAnchor = startOfToday()

const addDays = (date: Date, amount: number) => {
    const result = new Date(date)
    result.setDate(result.getDate() + amount)
    return result
}

const addMinutes = (date: Date, amount: number) => {
    const result = new Date(date)
    result.setMinutes(result.getMinutes() + amount)
    return result
}

const createFixtureEvent = (
    id: number,
    type: string,
    color: string,
    title: string,
    description: string,
    dayOffset: number,
    hour: number,
    minute: number,
    duration: number,
) => {
    const start = addDays(todayAnchor, dayOffset)
    start.setHours(hour, minute, 0, 0)
    const end = addMinutes(start, duration)

    return {
        id,
        type,
        color,
        title,
        description,
        startDate: start.toISOString(),
        endDate: end.toISOString(),
    }
}

const initialEvents = [
    createFixtureEvent(
        1,
        'fieldwork',
        'blue',
        'Site survey',
        'Walk the north lot and record the latest measurements.',
        0,
        8,
        20,
        45,
    ),
    createFixtureEvent(
        2,
        'planning',
        'purple',
        'Material review',
        'Compare the sample set and mark the next round of revisions.',
        0,
        8,
        45,
        90,
    ),
    createFixtureEvent(
        3,
        'operations',
        'green',
        'Print run planning',
        'Confirm quantities, paper stock, and the handoff sequence.',
        0,
        11,
        30,
        90,
    ),
    createFixtureEvent(
        4,
        'fieldwork',
        'blue',
        'Inventory count',
        'Reconcile the storage count before the afternoon dispatch.',
        0,
        15,
        0,
        20,
    ),
    createFixtureEvent(
        5,
        'planning',
        'purple',
        'Bench calibration',
        'Review calibration notes and approve the next test window.',
        0,
        16,
        10,
        50,
    ),
    createFixtureEvent(
        6,
        'operations',
        'green',
        'Route check',
        'Check the delivery route against the updated site notes.',
        -23,
        10,
        0,
        50,
    ),
    createFixtureEvent(
        7,
        'fieldwork',
        'blue',
        'Sample pickup',
        'Collect the prepared sample cases from the staging room.',
        -17,
        13,
        15,
        35,
    ),
    createFixtureEvent(
        8,
        'planning',
        'purple',
        'Layout review',
        'Review the second layout pass and leave focused notes.',
        -11,
        9,
        30,
        75,
    ),
    createFixtureEvent(
        9,
        'operations',
        'green',
        'Storage reset',
        'Reset the storage map after the incoming materials arrive.',
        -6,
        14,
        0,
        30,
    ),
    createFixtureEvent(
        10,
        'planning',
        'purple',
        'Quarter outline',
        'Shape the next quarter outline from the current field notes.',
        -2,
        16,
        30,
        45,
    ),
    createFixtureEvent(
        11,
        'fieldwork',
        'blue',
        'Workshop setup',
        'Prepare the tools and printed plans for the working session.',
        1,
        9,
        0,
        60,
    ),
    createFixtureEvent(
        12,
        'operations',
        'green',
        'Dispatch review',
        'Review the open dispatch list and close completed handoffs.',
        4,
        12,
        40,
        40,
    ),
    createFixtureEvent(
        13,
        'planning',
        'purple',
        'Archive planning',
        'Decide which notes should move into the project archive.',
        8,
        15,
        20,
        55,
    ),
    createFixtureEvent(
        14,
        'fieldwork',
        'blue',
        'Field notes handoff',
        'Hand off the annotated field notes for final review.',
        13,
        10,
        15,
        30,
    ),
    createFixtureEvent(
        15,
        'operations',
        'green',
        'Supply check',
        'Check the replenishment list and flag anything below threshold.',
        19,
        13,
        0,
        45,
    ),
]

type EventDraft = {
    title: string
    calendarId: string
    allDay: boolean
    startDate: Date
    endDate: Date
    startTime: Date
    endTime: Date
    description: string
}

const startOfDate = (date: Date) => {
    const result = new Date(date)
    result.setHours(0, 0, 0, 0)
    return result
}

const endOfDate = (date: Date) => {
    const result = new Date(date)
    result.setHours(23, 59, 59, 999)
    return result
}

const createDraft = (date: Date = new Date()): EventDraft => {
    const startDate = startOfDate(date)
    const startTime = new Date(date)

    if (startTime.getHours() === 0 && startTime.getMinutes() === 0) {
        startTime.setHours(9, 0, 0, 0)
    } else {
        startTime.setSeconds(0, 0)
    }

    const endTime = addMinutes(startTime, 30)

    return {
        title: '',
        calendarId: calendars[0].id,
        allDay: false,
        startDate,
        endDate: startOfDate(endTime),
        startTime,
        endTime,
        description: '',
    }
}

const isAllDayEvent = (event: FullCalendarEvent) => {
    const start = new Date(event.startDate)
    const end = new Date(event.endDate)
    const startsAtMidnight =
        start.getHours() === 0 && start.getMinutes() === 0
    const endsAtDayClose =
        end.getHours() === 23 &&
        end.getMinutes() === 59 &&
        end.getSeconds() === 59

    return startsAtMidnight && endsAtDayClose
}

const createDraftFromEvent = (event: FullCalendarEvent): EventDraft => {
    const start = new Date(event.startDate)
    const end = new Date(event.endDate)
    const allDay = isAllDayEvent(event)

    return {
        title: event.title,
        calendarId: event.type,
        allDay,
        startDate: startOfDate(start),
        endDate: startOfDate(end),
        startTime: start,
        endTime: end,
        description: event.description,
    }
}

const combineDateAndTime = (date: Date, time: Date) => {
    const combined = new Date(date)
    combined.setHours(time.getHours(), time.getMinutes(), 0, 0)
    return combined
}

const isSameDay = (left: Date | string, right: Date | string) => {
    const first = new Date(left)
    const second = new Date(right)
    return (
        first.getFullYear() === second.getFullYear() &&
        first.getMonth() === second.getMonth() &&
        first.getDate() === second.getDate()
    )
}

const formatDate = (date: Date | string) =>
    new Date(date).toLocaleDateString('en-US', {
        month: 'long',
        day: 'numeric',
        year: 'numeric',
    })

const formatWeekday = (date: Date | string) =>
    new Date(date).toLocaleDateString('en-US', { weekday: 'long' })

const formatTime = (date: Date | string) =>
    new Date(date).toLocaleTimeString('en-GB', {
        hour: '2-digit',
        minute: '2-digit',
        hour12: false,
    })

const getEventsForDay = (
    events: FullCalendarEvent[],
    selectedDate: Date,
) => {
    return events
        .filter((event) => isSameDay(event.startDate, selectedDate))
        .sort(
            (left, right) =>
                new Date(left.startDate).getTime() -
                new Date(right.startDate).getTime(),
        )
}

const getFirstEventForDay = (
    events: FullCalendarEvent[],
    selectedDate: Date,
) => getEventsForDay(events, selectedDate)[0] || null

const getNextEventForDay = (
    events: FullCalendarEvent[],
    selectedDate: Date,
) => {
    const dayEvents = getEventsForDay(events, selectedDate)

    return (
        dayEvents.find((event) => new Date(event.endDate) >= new Date()) ||
        dayEvents[0] ||
        null
    )
}

const renderMiniDay = (
    date: Date,
    selectedDate: Date,
    events: FullCalendarEvent[],
) => {
    const hasEvents = events.some((event) => isSameDay(event.startDate, date))
    const isSelected = isSameDay(date, selectedDate)

    return (
        <span className="relative flex h-full w-full items-center justify-center">
            <span>{date.getDate()}</span>
            {hasEvents && (
                <span
                    aria-hidden="true"
                    className={`absolute bottom-1 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full ${isSelected ? 'bg-primary-foreground' : 'bg-primary'}`}
                />
            )}
        </span>
    )
}

function GridDateSync({
    bind,
    selectedDate,
    setSelectedDate,
    onDateChange,
    view,
    onViewChange,
}: {
    bind: RefObject<((date: Date) => void) | null>
    selectedDate: Date
    setSelectedDate: (date: Date) => void
    onDateChange: (date: Date) => void
    view: CalendarView
    onViewChange: (view: CalendarView) => void
}) {
    useEffect(() => {
        bind.current = setSelectedDate

        return () => {
            bind.current = null
        }
    }, [bind, setSelectedDate])

    useEffect(() => {
        onDateChange(selectedDate)
    }, [onDateChange, selectedDate])

    useEffect(() => {
        onViewChange(view)
    }, [onViewChange, view])

    return null
}

export default function CalendarDayFocusRail() {
    const [calendarEvents, setCalendarEvents] = useState(initialEvents)
    const [selectedEventId, setSelectedEventId] = useState<number | null>(null)
    const [guestIdsByEvent, setGuestIdsByEvent] =
        useState<Record<number, string[]>>(initialGuestIds)
    const [isEventDrawerOpen, setEventDrawerOpen] = useState(false)
    const [editingEventId, setEditingEventId] = useState<number | null>(null)
    const [eventToDelete, setEventToDelete] =
        useState<FullCalendarEvent | null>(null)
    const [activeDate, setActiveDate] = useState(todayAnchor)
    const [activeView, setActiveView] = useState<CalendarView>('day')
    const [eventDraft, setEventDraft] = useState<EventDraft>(() =>
        createDraft(todayAnchor),
    )
    const [formError, setFormError] = useState<string | null>(null)
    const setCalendarDateRef = useRef<((date: Date) => void) | null>(null)
    const calendarEventsRef = useRef(calendarEvents)

    useEffect(() => {
        calendarEventsRef.current = calendarEvents
    }, [calendarEvents])

    useEffect(() => {
        if (activeView !== 'day') return

        // DayView has no initial-scroll prop, so mirror WeekView's one-hour
        // lookback against its Scroll viewport from this single DOM bridge.
        const frame = window.requestAnimationFrame(() => {
            const viewport = document.querySelector<HTMLElement>(
                'section .scroll-viewport',
            )
            if (!viewport) return

            const firstEvent = getFirstEventForDay(
                calendarEventsRef.current,
                activeDate,
            )
            const anchor = firstEvent
                ? new Date(firstEvent.startDate)
                : new Date()
            const anchorMinutes =
                anchor.getHours() * 60 + anchor.getMinutes()
            const targetMinutes = Math.max(0, anchorMinutes - 60)
            const targetTop = (targetMinutes / 60) * 96
            const maxScrollTop = Math.max(
                0,
                viewport.scrollHeight - viewport.clientHeight,
            )

            viewport.scrollTop = Math.min(targetTop, maxScrollTop)
        })

        return () => window.cancelAnimationFrame(frame)
    }, [activeDate, activeView])

    const isEditingEvent = editingEventId !== null

    const openCreateDrawer = (date: Date = new Date()) => {
        setEventDraft(createDraft(date))
        setFormError(null)
        setEditingEventId(null)
        setEventDrawerOpen(true)
    }

    const openEditDrawer = (event: FullCalendarEvent) => {
        setEventDraft(createDraftFromEvent(event))
        setFormError(null)
        setEditingEventId(event.id)
        setEventDrawerOpen(true)
    }

    const closeEventDrawer = () => {
        setEventDrawerOpen(false)
        setEditingEventId(null)
        setFormError(null)
    }

    const updateEventDraft = <Key extends keyof EventDraft>(
        key: Key,
        value: EventDraft[Key],
    ) => {
        setEventDraft((currentDraft) => ({
            ...currentDraft,
            [key]: value,
        }))
        setFormError(null)
    }

    const handleSaveEvent = () => {
        const title = eventDraft.title.trim()
        const calendar = calendars.find(
            (item) => item.id === eventDraft.calendarId,
        )
        const startDateTime = combineDateAndTime(
            eventDraft.startDate,
            eventDraft.startTime,
        )
        const endDateTime = eventDraft.allDay
            ? endOfDate(eventDraft.endDate)
            : combineDateAndTime(eventDraft.endDate, eventDraft.endTime)

        if (!title) {
            setFormError('Add a title before saving the event.')
            return
        }

        if (!calendar) {
            setFormError('Choose a calendar before saving the event.')
            return
        }

        if (endDateTime <= startDateTime) {
            setFormError('The end must be after the start.')
            return
        }

        const nextEventId =
            editingEventId ??
            Math.max(0, ...calendarEvents.map((event) => event.id)) + 1

        setCalendarEvents((currentEvents) => {
            if (editingEventId !== null) {
                return currentEvents.map((event) =>
                    event.id === editingEventId
                        ? {
                              ...event,
                              type: calendar.id,
                              color: calendar.color,
                              title,
                              description: eventDraft.description.trim(),
                              startDate: startDateTime.toISOString(),
                              endDate: endDateTime.toISOString(),
                          }
                        : event,
                )
            }

            return [
                ...currentEvents,
                {
                    id: nextEventId,
                    type: calendar.id,
                    color: calendar.color,
                    title,
                    description: eventDraft.description.trim(),
                    startDate: startDateTime.toISOString(),
                    endDate: endDateTime.toISOString(),
                },
            ]
        })

        if (editingEventId === null) {
            setGuestIdsByEvent((currentGuests) => ({
                ...currentGuests,
                [nextEventId]: [],
            }))
        }
        setSelectedEventId(nextEventId)
        closeEventDrawer()
    }

    const handleCalendarDateChange = (
        date: Date,
        setSelectedDate: (date: Date) => void,
    ) => {
        setSelectedEventId(null)
        setSelectedDate(date)
    }

    const handleCellClick = (date: Date) => {
        setCalendarDateRef.current?.(date)
        setSelectedEventId(null)
        openCreateDrawer(date)
    }

    const handleCalendarChange = (
        _events: FullCalendarEvent[],
        updatedEvent: FullCalendarEvent,
    ) => {
        setCalendarEvents((currentEvents) =>
            currentEvents.map((event) =>
                event.id === updatedEvent.id ? updatedEvent : event,
            ),
        )
    }

    const handleDelete = () => {
        if (!eventToDelete) return

        const eventId = eventToDelete.id
        setCalendarEvents((currentEvents) =>
            currentEvents.filter((event) => event.id !== eventId),
        )
        setGuestIdsByEvent((currentGuests) => {
            const nextGuests = { ...currentGuests }
            delete nextGuests[eventId]
            return nextGuests
        })
        setSelectedEventId(null)
        setEventToDelete(null)
    }

    const getGuestIds = (eventId: number) => guestIdsByEvent[eventId] ?? []

    const toggleGuest = (
        eventId: number,
        guestId: string,
        checked: boolean,
    ) => {
        setGuestIdsByEvent((currentGuests) => {
            const currentIds = currentGuests[eventId] ?? []
            const nextIds = checked
                ? Array.from(new Set([...currentIds, guestId]))
                : currentIds.filter((id) => id !== guestId)

            return {
                ...currentGuests,
                [eventId]: nextIds,
            }
        })
    }

    const renderHeaderStart = ({
        selectedDate,
        setSelectedDate,
        view,
    }: {
        selectedDate: Date
        setSelectedDate: (date: Date) => void
        view: CalendarView
    }) => {
        return (
            <>
                <GridDateSync
                    bind={setCalendarDateRef}
                    selectedDate={selectedDate}
                    setSelectedDate={setSelectedDate}
                    onDateChange={setActiveDate}
                    view={view}
                    onViewChange={setActiveView}
                />
                <div className="flex min-w-0 items-center gap-4">
                    <div className="flex h-12 w-12 shrink-0 flex-col items-center justify-between rounded-control border bg-card text-card-foreground overflow-hidden">
                        <span className="text-[10px] font-medium uppercase bg-muted w-full h-4 text-center items-center">
                            {selectedDate.toLocaleDateString('en-US', {
                                month: 'short',
                            })}
                        </span>
                        <span className="font-semibold flex-1 flex items-center">
                            {selectedDate.getDate()}
                        </span>
                    </div>
                    <div className="min-w-0">
                        <h5 id={headingId} className="truncate">
                            {formatDate(selectedDate)}
                        </h5>
                        <p className="text-xs text-muted-foreground">
                            {formatWeekday(selectedDate)}
                        </p>
                    </div>
                </div>
            </>
        )
    }

    const renderHeaderEnd = ({
        handlePrevious,
        handleNext,
        selectedDate,
        setSelectedDate,
        view,
        setView,
    }: {
        handlePrevious: () => void
        handleNext: () => void
        selectedDate: Date
        setSelectedDate: (date: Date) => void
        view: CalendarView
        setView: (view: CalendarView) => void
    }) => {
        return (
            <div className="flex flex-wrap items-center gap-2">
                <div className="flex items-center gap-1">
                    <InputGroup>
                        <Button
                            aria-label="Previous period"
                            icon={<PiCaretLeft aria-hidden="true" />}
                            size="sm"
                            onClick={() => {
                                setSelectedEventId(null)
                                handlePrevious()
                            }}
                        />
                        <Button
                            size="sm"
                            variant="default"
                            onClick={() => {
                                setSelectedEventId(null)
                                setSelectedDate(new Date())
                            }}
                        >
                            Today
                        </Button>
                        <Button
                            aria-label="Next period"
                            icon={<PiCaretRight aria-hidden="true" />}
                            size="sm"
                            onClick={() => {
                                setSelectedEventId(null)
                                handleNext()
                            }}
                        />
                    </InputGroup>
                </div>
                <Select
                    className="w-28"
                    options={viewOptions}
                    size="sm"
                    value={viewOptions.find((option) => option.value === view)}
                    onChange={(selected) => {
                        if (selected && 'value' in selected) {
                            setView(selected.value as CalendarView)
                        }
                    }}
                />
                <Button
                    icon={<PiPlus aria-hidden="true" />}
                    size="sm"
                    variant="solid"
                    onClick={() => openCreateDrawer(selectedDate)}
                >
                    Add event
                </Button>
            </div>
        )
    }

    return (
        <section
            aria-labelledby={headingId}
            className="flex h-full min-h-0 max-h-dvh min-w-0 flex-col overflow-hidden"
        >
            <div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
                <div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
                    <FullCalendar
                    fillHeight
                    events={calendarEvents}
                    view="day"
                    onCellClick={handleCellClick}
                    onEventClick={(event) => setSelectedEventId(event.id)}
                    onChange={handleCalendarChange}
                    renderEvent={({ event }) => {
                        const start = new Date(event.startDate)
                        const end = new Date(event.endDate)
                        const durationMinutes = Math.round(
                            (end.getTime() - start.getTime()) / 60000,
                        )

                        return {
                            className:
                                eventTintClass[event.color] ??
                                eventTintClass.blue,
                            content: (
                                <>
                                    <div className="flex items-center gap-2 truncate">
                                        <p className="truncate font-medium">
                                            {event.title}
                                        </p>
                                    </div>
                                    {durationMinutes > 25 && (
                                        <p>
                                            {formatTime(start)} - {formatTime(end)}
                                        </p>
                                    )}
                                </>
                            ),
                        }
                    }}
                    renderHeaderStart={renderHeaderStart}
                    renderHeaderEnd={renderHeaderEnd}
                    renderDayViewSidebar={({ selectedDate, setSelectedDate }) => {
                        const activeEvent = getNextEventForDay(
                            calendarEvents,
                            selectedDate,
                        )
                        const selectedEvent = calendarEvents.find(
                            (event) =>
                                event.id === selectedEventId &&
                                isSameDay(event.startDate, selectedDate),
                        )
                        const detailEvent = selectedEvent || activeEvent
                        const selectedGuestIds = detailEvent
                            ? getGuestIds(detailEvent.id)
                            : []
                        const guests = guestOptions.filter((guest) =>
                            selectedGuestIds.includes(guest.id),
                        )

                        return (
                            <div className="hidden min-h-0 min-w-0 w-72 shrink-0 flex-col border-l bg-card md:flex">
                                <div className="shrink-0 border-b p-4">
                                    <Calendar
                                        className="w-full min-w-0"
                                        firstDayOfWeek="monday"
                                        labelFormat={{
                                            month: 'MMMM',
                                            year: 'YYYY',
                                        }}
                                        renderDay={(date) =>
                                            renderMiniDay(
                                                date,
                                                selectedDate,
                                                calendarEvents,
                                            )
                                        }
                                        value={selectedDate}
                                        onChange={(date) => {
                                            if (date) {
                                                handleCalendarDateChange(
                                                    date,
                                                    setSelectedDate,
                                                )
                                            }
                                        }}
                                    />
                                </div>
                                <Scroll
                                    className="min-h-0 flex-1"
                                    scrollbars="vertical"
                                    type="always"
                                >
                                    <div className="p-4">
                                        {detailEvent ? (
                                            <div>
                                                <div className="flex items-start justify-between gap-2">
                                                    <h5 className="text-lg font-sembold min-w-0 truncate">
                                                        {detailEvent.title}
                                                    </h5>
                                                    <div className="flex shrink-0 items-center gap-1">
                                                        <Button
                                                            aria-label="Delete event"
                                                            destructive
                                                            icon={<PiTrash aria-hidden="true" />}
                                                            size="sm"
                                                            variant="ghost"
                                                            onClick={() =>
                                                                setEventToDelete(
                                                                    detailEvent,
                                                                )
                                                            }
                                                        />
                                                        <Button
                                                            aria-label="Edit event"
                                                            icon={<PiPencilSimple aria-hidden="true" />}
                                                            size="sm"
                                                            variant="ghost"
                                                            onClick={() =>
                                                                openEditDrawer(
                                                                    detailEvent,
                                                                )
                                                            }
                                                        />
                                                    </div>
                                                </div>

                                                <div className="mt-4 space-y-4">
                                                    <div className="flex items-center gap-2">
                                                        <PiCalendarBlank
                                                            aria-hidden="true"
                                                            className="shrink-0 text-lg text-muted-foreground"
                                                        />
                                                        <span>
                                                            {formatDate(
                                                                detailEvent.startDate,
                                                            )}
                                                        </span>
                                                    </div>
                                                    <div className="flex items-center gap-2">
                                                        <PiClock
                                                            aria-hidden="true"
                                                            className="shrink-0 text-lg text-muted-foreground"
                                                        />
                                                        <span>
                                                            {isAllDayEvent(
                                                                detailEvent,
                                                            )
                                                                ? 'All day'
                                                                : `${formatTime(detailEvent.startDate)} - ${formatTime(detailEvent.endDate)}`}
                                                        </span>
                                                    </div>
                                                </div>

                                                <div className="mt-4 flex items-center justify-between gap-2 border-t pt-4">
                                                    <div className="flex min-w-0 items-center">
                                                        <div className="flex shrink-0 items-center">
                                                            {guests.map(
                                                                (guest, index) => (
                                                                    <Avatar
                                                                        key={guest.id}
                                                                        alt=""
                                                                        className={
                                                                            index >
                                                                                0
                                                                                ? '-ml-2'
                                                                                : ''
                                                                        }
                                                                        size={28}
                                                                        src={guest.avatar}
                                                                    />
                                                                ),
                                                            )}
                                                        </div>
                                                        <Popover
                                                            placement="top-end"
                                                            trigger="click"
                                                            width={260}
                                                            className="p-2"
                                                            renderTrigger={
                                                                <Button
                                                                    aria-label="Manage guests"
                                                                    className="ml-2"
                                                                    icon={<PiPlus aria-hidden="true" />}
                                                                    shape="circle"
                                                                    size="sm"
                                                                    variant="subtle"
                                                                />
                                                            }
                                                        >
                                                            <div>
                                                                {guestOptions.map(
                                                                    (guest) => {
                                                                        const checked = selectedGuestIds.includes(
                                                                            guest.id,
                                                                        )

                                                                        return (
                                                                            <button
                                                                                aria-pressed={checked}
                                                                                key={guest.id}
                                                                                type="button"
                                                                                className="flex cursor-pointer items-center gap-2 rounded-control-sm px-2 py-2 hover:bg-accent w-full"
                                                                                onClick={() =>
                                                                                    toggleGuest(
                                                                                        detailEvent.id,
                                                                                        guest.id,
                                                                                        !checked,
                                                                                    )
                                                                                }
                                                                            >
                                                                                <span className="flex items-center gap-2">
                                                                                    <Avatar
                                                                                        alt=""
                                                                                        size={24}
                                                                                        src={guest.avatar}
                                                                                    />
                                                                                    <span className="min-w-0 truncate">
                                                                                        {guest.name}
                                                                                    </span>
                                                                                </span>
                                                                                {checked && (
                                                                                    <PiCheck
                                                                                        aria-hidden="true"
                                                                                        className="ml-auto shrink-0 text-lg text-primary"
                                                                                    />
                                                                                )}
                                                                            </button>
                                                                        )
                                                                    },
                                                                )}
                                                            </div>
                                                        </Popover>
                                                    </div>
                                                    <span className="shrink-0 text-muted-foreground">
                                                        {selectedGuestIds.length}{' '}
                                                        guests
                                                    </span>
                                                </div>

                                                <div className="mt-4 border-t pt-4">
                                                    <div className="font-semibold">
                                                        Description
                                                    </div>
                                                    <p className="mt-2 text-muted-foreground">
                                                        {detailEvent.description ||
                                                            'No description yet.'}
                                                    </p>
                                                </div>
                                            </div>
                                        ) : (
                                            <div className="flex items-center justify-center py-4">
                                                <EmptyState
                                                    illustration={
                                                        <IconFrame>
                                                            <PiCalendarBlank
                                                                aria-hidden="true"
                                                                className="text-2xl"
                                                            />
                                                        </IconFrame>
                                                    }
                                                    size={180}
                                                    offset={-40}
                                                    variant="dots"
                                                >
                                                    <div className="flex max-w-52 flex-col items-center text-center">
                                                        <h6 className="font-semibold text-base">
                                                            Nothing scheduled
                                                        </h6>
                                                        <p className="text-muted-foreground mt-1">
                                                            Add an event to start
                                                            planning this day.
                                                        </p>
                                                        <Button
                                                            className="mt-4"
                                                            icon={<PiPlus aria-hidden="true" />}
                                                            onClick={() =>
                                                                openCreateDrawer(
                                                                    selectedDate,
                                                                )
                                                            }
                                                        >
                                                            Add event
                                                        </Button>
                                                    </div>
                                                </EmptyState>
                                            </div>
                                        )}
                                    </div>
                                </Scroll>
                            </div>
                        )
                    }}
                    />
                </div>
            </div>

            <Drawer
                isOpen={isEventDrawerOpen}
                placement="right"
                title={isEditingEvent ? 'Edit event' : 'New event'}
                onClose={closeEventDrawer}
                footerClass="flex justify-end gap-2"
                footer={
                    <>
                        <Button type="button" onClick={closeEventDrawer}>
                            Cancel
                        </Button>
                        <Button
                            form={eventFormId}
                            type="submit"
                            variant="solid"
                        >
                            {isEditingEvent ? 'Save changes' : 'Create event'}
                        </Button>
                    </>
                }
            >
                <Form
                    id={eventFormId}
                    containerClassName="space-y-4"
                    onSubmit={(event) => {
                        event.preventDefault()
                        handleSaveEvent()
                    }}
                >
                    {formError && (
                        <p
                            className="rounded-control bg-destructive-soft px-3 py-2 text-destructive"
                            role="alert"
                        >
                            {formError}
                        </p>
                    )}

                    <div>
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-title"
                            label="Title"
                        >
                            <Input
                                id="calendar-day-focus-title"
                                placeholder="Add event title"
                                value={eventDraft.title}
                                onChange={(event) =>
                                    updateEventDraft('title', event.target.value)
                                }
                            />
                        </Form.Field>
                    </div>

                    <div>
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-calendar"
                            label="Calendar"
                        >
                            <Select
                                inputId="calendar-day-focus-calendar"
                                options={calendarOptions}
                                value={calendarOptions.find(
                                    (option) =>
                                        option.value === eventDraft.calendarId,
                                )}
                                customInputDisplay={(selected) => {
                                    const calendar = calendars.find(
                                        (item) => item.id === selected?.value,
                                    )

                                    return calendar ? (
                                        <span className="flex items-center gap-2">
                                            <span
                                                aria-hidden="true"
                                                className={`size-2 rounded-full ${swatchClass[calendar.color]}`}
                                            />
                                            <span>{calendar.label}</span>
                                        </span>
                                    ) : null
                                }}
                                customOption={({ option, selected, CheckIcon }) => {
                                    const calendar = calendars.find(
                                        (item) => item.id === option.value,
                                    )

                                    return (
                                        <span className="flex w-full items-center justify-between gap-2">
                                            <span className="flex min-w-0 items-center gap-2">
                                                {calendar && (
                                                    <span
                                                        aria-hidden="true"
                                                        className={`size-2 shrink-0 rounded-full ${swatchClass[calendar.color]}`}
                                                    />
                                                )}
                                                <span className="truncate">
                                                    {option.label}
                                                </span>
                                            </span>
                                            {selected && CheckIcon}
                                        </span>
                                    )
                                }}
                                onChange={(selected) => {
                                    if (selected && 'value' in selected) {
                                        updateEventDraft(
                                            'calendarId',
                                            selected.value as string,
                                        )
                                    }
                                }}
                            />
                        </Form.Field>
                    </div>

                    <div className="flex items-center justify-between gap-2">
                        <span className="font-medium text-foreground">All day</span>
                        <Switcher
                            aria-label="All day"
                            checked={eventDraft.allDay}
                            onChange={(checked) =>
                                updateEventDraft('allDay', checked)
                            }
                        />
                    </div>

                    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-start-date"
                            label="Starts"
                        >
                            <DatePicker
                                id="calendar-day-focus-start-date"
                                inputFormat="MMMM D, YYYY"
                                value={eventDraft.startDate}
                                clearable={false}
                                onChange={(date) => {
                                    if (date) {
                                        updateEventDraft('startDate', date)
                                    }
                                }}
                            />
                        </Form.Field>
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-end-date"
                            label="Ends"
                        >
                            <DatePicker
                                id="calendar-day-focus-end-date"
                                inputFormat="MMMM D, YYYY"
                                value={eventDraft.endDate}
                                clearable={false}
                                onChange={(date) => {
                                    if (date) {
                                        updateEventDraft('endDate', date)
                                    }
                                }}
                            />
                        </Form.Field>
                    </div>

                    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-start-time"
                            label="From"
                        >
                            <TimeInput
                                id="calendar-day-focus-start-time"
                                value={eventDraft.startTime}
                                format="24"
                                clearable={false}
                                disabled={eventDraft.allDay}
                                onChange={(time) => {
                                    if (time) {
                                        updateEventDraft('startTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-end-time"
                            label="To"
                        >
                            <TimeInput
                                id="calendar-day-focus-end-time"
                                value={eventDraft.endTime}
                                format="24"
                                clearable={false}
                                disabled={eventDraft.allDay}
                                onChange={(time) => {
                                    if (time) {
                                        updateEventDraft('endTime', time)
                                    }
                                }}
                            />
                        </Form.Field>
                    </div>

                    <div>
                        <Form.Field
                            className="mb-0"
                            htmlFor="calendar-day-focus-description"
                            label="Description"
                        >
                            <Input
                                id="calendar-day-focus-description"
                                placeholder="Add notes or an agenda"
                                rows={4}
                                textArea
                                value={eventDraft.description}
                                onChange={(event) =>
                                    updateEventDraft(
                                        'description',
                                        event.target.value,
                                    )
                                }
                            />
                        </Form.Field>
                    </div>
                </Form>
            </Drawer>

            <Dialog
                isOpen={eventToDelete !== null}
                aria-describedby="calendar-day-focus-delete-description"
                aria-labelledby="calendar-day-focus-delete-title"
                width={420}
                onClose={() => setEventToDelete(null)}
            >
                <div className="space-y-4">
                    <div>
                        <h5 id="calendar-day-focus-delete-title">
                            Delete event
                        </h5>
                        <p
                            id="calendar-day-focus-delete-description"
                            className="mt-2 text-muted-foreground"
                        >
                            Delete &quot;{eventToDelete?.title}&quot;? This cannot
                            be undone.
                        </p>
                    </div>
                    <div className="flex justify-end gap-2">
                        <Button
                            type="button"
                            onClick={() => setEventToDelete(null)}
                        >
                            Cancel
                        </Button>
                        <Button
                            destructive
                            type="button"
                            variant="solid"
                            onClick={handleDelete}
                        >
                            Delete event
                        </Button>
                    </div>
                </div>
            </Dialog>
        </section>
    )
}