event-calendar

Event calendar with all views and live settings

Event calendar with all views and live settings

Загрузка превью…

src/event-calendar/c-event-calendar-1.vue

<!--
  ПОРТ c-event-calendar-1 ("Event calendar with all views and live settings").

  Апстрим держит `view` в React `useState` и меняет `viewSettings`/
  `interactions`/`weekStartsOn`/`dayStartHour`/`dayEndHour`/`interval`/
  `snapDuration`/`locale`/`timeZone` контролируемо (родитель хранит
  `DemoSettings`, `EventCalendar` перерендеривается с новыми пропами).
  Наш `EventCalendar.vue` v1 — только неуправляемый движок (`context.ts`,
  `createEventCalendar` читает `options` один раз при создании), поэтому
  такая живая перепривязка пропов в принципе не работает так же, как в
  React. Настройки, для которых есть императивный сеттер на `api`
  (`setViewSettings`/`setInteractions`), реализованы по-настоящему через
  `calRef`. Остальные разделы демо-панели (Time grid: day start/end/
  interval/snap; Region: language/timeZone; Week starts on; Day add button)
  сеттера на движке не имеют — сознательно не включены в порт вместо
  притворно интерактивных мёртвых контролов (правило 4a: не изображаем
  работающее поведение, которого на самом деле нет). Popover закрыт по
  умолчанию и не раскрывается диф-гейтом (см. docs/PORTING.md §10), поэтому
  сокращение состава здесь не влияет на визуальное сравнение.

  IconPlaceholder (Settings/Plus) заменён инлайновым `<svg>` (пути lucide
  "settings-2"/"plus" v0.545.0), тем же приёмом, что и везде в порте.
  `EventCalendarNav` — настоящий порт (без view switcher/date picker,
  см. EventCalendarNav.vue), `EventCalendarToolbar` — простая обёртка
  `flex items-center gap-2`, инлайнена ниже вместо отдельного компонента
  (не экспортируется портом, см. комментарий в EventCalendarNav.vue).
  `timeZone="UTC"` и фиксированный якорь `Date.UTC(2026, 5, 15)` — по тем же
  причинам, что и в кейсе `ui-event-calendar` (стенд крутится в UTC,
  "сегодня" недетерминировано). `renderEvent`/`eventTooltip`/`maxEventsPerCell`
  (3 вместо дефолтного "auto" — ResizeObserver-измерение недетерминировано на
  статичном скриншоте, тот же обход, что и в `ui-event-calendar`) переданы
  через новый `view-config`-проп `EventCalendar.vue` (root-level оверрайд
  view-layer конфига, добавлен этой задачей — см. комментарий в
  `EventCalendar.vue`), а не отдельными плоскими пропами, как в оригинале.
-->
<script setup lang="ts">
import { h, reactive, ref } from "vue"
import {
  addDays,
  addMinutes,
  setHours,
  startOfDay,
  startOfWeek,
} from "date-fns"
import { EventCalendar, EventCalendarContent, EventCalendarNav, type CalendarEvent, type EventCalendarApi, type EventCalendarResource, type EventCalendarRenderEventProps } from "@/components/reui/event-calendar"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const ANCHOR = new Date(Date.UTC(2026, 5, 15))

const TEAM: EventCalendarResource[] = [
  { id: "alex", title: "Alex", color: "var(--color-blue-500)" },
  { id: "mia", title: "Mia", color: "var(--color-violet-500)" },
  { id: "sam", title: "Sam", color: "var(--color-emerald-500)" },
]

function buildEvents(anchor: Date): CalendarEvent[] {
  const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
  const at = (dayOffset: number, hour: number, minute = 0) =>
    addMinutes(setHours(addDays(week, dayOffset), hour), minute)
  const day = (dayOffset: number) => addDays(week, dayOffset)

  return [
    { id: "team-sync", title: "Team sync", start: at(1, 9, 0), end: at(1, 9, 30), resourceId: "alex" },
    { id: "design-review", title: "Design review", start: at(2, 11, 0), end: at(2, 12, 0), resourceId: "mia", color: "var(--color-violet-500)" },
    { id: "product-demo", title: "Product demo", start: at(3, 15, 0), end: at(3, 16, 0), resourceId: "sam", color: "var(--color-emerald-500)" },
    { id: "roadmap-planning", title: "Roadmap planning", start: at(4, 10, 0), end: at(4, 11, 30), resourceId: "alex", color: "var(--color-indigo-500)" },
    { id: "client-call", title: "Client call", start: at(5, 14, 0), end: at(5, 15, 0), resourceId: "mia", color: "var(--color-amber-500)" },
    { id: "team-offsite", title: "Team offsite", start: day(4), end: day(6), allDay: true, color: "var(--color-rose-500)" },
    { id: "sprint-planning", title: "Sprint planning", start: at(9, 9, 30), end: at(9, 10, 30), resourceId: "sam", color: "var(--color-blue-500)" },
    { id: "quarterly-review", title: "Quarterly review", start: at(17, 13, 0), end: at(17, 14, 30), resourceId: "alex", color: "var(--color-cyan-500)" },
  ]
}

const events = buildEvents(ANCHOR)

function renderEventContent({ occurrence }: EventCalendarRenderEventProps) {
  const event = occurrence.event

  if (event.id === "design-review") {
    return [
      h("span", { class: "flex shrink-0 -space-x-1" }, [
        h(Avatar, { class: "ring-background size-4 ring-1" }, () =>
          h(AvatarFallback, { class: "bg-violet-500 text-[8px] font-semibold text-white" }, () => "MJ")
        ),
        h(Avatar, { class: "ring-background size-4 ring-1" }, () =>
          h(AvatarFallback, { class: "bg-sky-500 text-[8px] font-semibold text-white" }, () => "AL")
        ),
      ]),
      h("span", { class: "truncate font-medium" }, event.title),
    ]
  }

  if (event.id === "client-call") {
    return h("span", { class: "flex w-full min-w-0 items-center gap-1.5" }, [
      h("span", { "aria-hidden": "true", class: "-me-0.5 size-1.5 shrink-0 rounded-full bg-(--ec-event-color)" }),
      h("span", { class: "truncate font-medium" }, event.title),
      h("span", { class: "ms-auto shrink-0 rounded bg-(--ec-event-color)/25 px-1 text-[10px] font-semibold" }, "30m"),
    ])
  }

  return undefined
}

const calRef = ref<{ api: EventCalendarApi } | null>(null)
let newEventCount = 0

const viewSettings = reactive({ weekends: true, weekNumbers: false, nowIndicator: true, offDays: false })
const interactions = reactive({ drag: true, resize: true, selectSlot: true })
const eventTooltip = ref(false)

function patchViewSettings(patch: Partial<typeof viewSettings>) {
  Object.assign(viewSettings, patch)
  calRef.value?.api.setViewSettings({ ...viewSettings })
}
function patchInteractions(patch: Partial<typeof interactions>) {
  Object.assign(interactions, patch)
  calRef.value?.api.setInteractions({ ...interactions })
}

function addEvent() {
  const start = setHours(startOfDay(ANCHOR), 12)
  const end = addMinutes(start, 60)
  calRef.value?.api.addEvent({
    id: `new-event-${newEventCount++}`,
    title: "New event",
    start,
    end,
    resourceId: "alex",
    color: "var(--color-blue-500)",
  })
  calRef.value?.api.goTo(start)
}
</script>

<template>
  <div class="w-full p-4">
    <Card class="w-full py-0">
      <CardContent class="p-0">
        <EventCalendar
          ref="calRef"
          :events="events"
          view="month"
          :resources="TEAM"
          :view-config="{ renderEvent: renderEventContent, eventTooltip, maxEventsPerCell: 3 }"
          time-zone="UTC"
          :view-settings="viewSettings"
          :interactions="interactions"
          off-days
          class="h-[640px] w-full"
        >
          <div class="flex flex-wrap items-center gap-2 pe-2">
            <EventCalendarNav class="min-w-0 flex-1" />
            <div data-slot="event-calendar-toolbar" class="flex items-center gap-2">
              <Popover>
                <PopoverTrigger as-child>
                  <Button variant="outline" size="sm">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M14 17H5" /><path d="M19 7h-9" /><circle cx="17" cy="17" r="3" /><circle cx="7" cy="7" r="3" /></svg>
                    Settings
                  </Button>
                </PopoverTrigger>
                <PopoverContent align="end" :side-offset="8" class="w-80">
                  <Tabs default-value="view">
                    <TabsList class="w-full">
                      <TabsTrigger value="view" class="flex-1">View</TabsTrigger>
                      <TabsTrigger value="behavior" class="flex-1">Behavior</TabsTrigger>
                    </TabsList>
                    <TabsContent value="view" class="flex flex-col gap-3">
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-weekends" class="font-normal">Weekends</Label>
                        <Switch id="ec-set-weekends" :model-value="viewSettings.weekends" @update:model-value="(v: boolean) => patchViewSettings({ weekends: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-week-numbers" class="font-normal">Week numbers</Label>
                        <Switch id="ec-set-week-numbers" :model-value="viewSettings.weekNumbers" @update:model-value="(v: boolean) => patchViewSettings({ weekNumbers: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-now" class="font-normal">Now indicator</Label>
                        <Switch id="ec-set-now" :model-value="viewSettings.nowIndicator" @update:model-value="(v: boolean) => patchViewSettings({ nowIndicator: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-off-days" class="font-normal">Mark off days</Label>
                        <Switch id="ec-set-off-days" :model-value="viewSettings.offDays" @update:model-value="(v: boolean) => patchViewSettings({ offDays: v })" />
                      </div>
                    </TabsContent>
                    <TabsContent value="behavior" class="flex flex-col gap-3">
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-drag" class="font-normal">Drag to move</Label>
                        <Switch id="ec-set-drag" :model-value="interactions.drag" @update:model-value="(v: boolean) => patchInteractions({ drag: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-resize" class="font-normal">Drag to resize</Label>
                        <Switch id="ec-set-resize" :model-value="interactions.resize" @update:model-value="(v: boolean) => patchInteractions({ resize: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-select-slot" class="font-normal">Drag to create</Label>
                        <Switch id="ec-set-select-slot" :model-value="interactions.selectSlot" @update:model-value="(v: boolean) => patchInteractions({ selectSlot: v })" />
                      </div>
                      <div class="flex items-center justify-between gap-4">
                        <Label for="ec-set-tooltip" class="font-normal">Event tooltips</Label>
                        <Switch id="ec-set-tooltip" :model-value="eventTooltip" @update:model-value="(v: boolean) => (eventTooltip = v)" />
                      </div>
                    </TabsContent>
                  </Tabs>
                </PopoverContent>
              </Popover>
              <Button size="sm" @click="addEvent">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
                New event
              </Button>
            </div>
          </div>
          <EventCalendarContent />
        </EventCalendar>
      </CardContent>
    </Card>
  </div>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-event-calendar-1.json

npm-зависимости

  • date-fns

Источник: порт из ReUI (Keenthemes, MIT)