"use client"; import { useEffect, useState } from "react"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { ActionIcon, Button, Card, Fieldset, Group, Select, Stack, Switch, Text, TextInput } from "@mantine/core"; import { DateTimePicker } from "@mantine/dates"; import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react"; import dayjs from "dayjs"; import { createId } from "@homarr/common"; import type { WidgetKind } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; import { formatWallDateTime, resolveWallDateTimeString } from "../countdown/recurrence"; import type { DateTimeEventRecurrence, OptionDateTimeEvent } from "../options"; import type { CommonWidgetInputProps } from "./common"; import { useWidgetInputTranslation } from "./common"; import { useFormContext } from "./form"; export const WidgetDateTimeEventListInput = ({ property, kind, options, }: CommonWidgetInputProps<"dateTimeEventList">) => { const t = useWidgetInputTranslation(kind, property); const tEditor = useEventListTranslation(kind); const form = useFormContext(); const fieldPath = `options.${property}`; const currentValue = form.values.options[property]; const values = Array.isArray(currentValue) ? (currentValue as OptionDateTimeEvent[]) : options.defaultValue; const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); useEffect(() => { if (Array.isArray(currentValue)) return; form.setFieldValue(fieldPath, options.defaultValue); }, [currentValue, fieldPath, form, options.defaultValue]); const setValues = (nextValues: OptionDateTimeEvent[]) => form.setFieldValue(fieldPath, nextValues); const addEvent = () => { if (values.length >= options.maxValues) return; const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; const target = dayjs().add(1, "day").minute(0).second(0).millisecond(0); setValues([ ...values, { id: createId(), label: tEditor("newEvent"), targetUtc: target.toISOString(), timeZone: localTimeZone, recurrence: "none", }, ]); }; return (
{options.withDescription && ( {t("description")} )} { if (!over || active.id === over.id) return; const activeIndex = values.findIndex(({ id }) => id === active.id); const overIndex = values.findIndex(({ id }) => id === over.id); if (activeIndex < 0 || overIndex < 0) return; setValues(arrayMove(values, activeIndex, overIndex)); }} > id)} strategy={verticalListSortingStrategy}> {values.map((event) => ( setValues(values.map((candidate) => (candidate.id === event.id ? nextEvent : candidate))) } onRemove={() => setValues(values.filter((candidate) => candidate.id !== event.id))} /> ))} {tEditor("limit", { count: values.length, maximum: options.maxValues })}
); }; interface SortableEventProps { kind: WidgetKind; event: OptionDateTimeEvent; timeZoneOptions: readonly { value: string; label: string }[]; onChange: (event: OptionDateTimeEvent) => void; onRemove: () => void; } const SortableEvent = ({ kind, event, timeZoneOptions, onChange, onRemove }: SortableEventProps) => { const t = useEventListTranslation(kind); const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({ id: event.id, }); const style = { transform: transform ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)` : undefined, transition, opacity: isDragging ? 0.6 : undefined, zIndex: isDragging ? 1 : undefined, }; const hasCurrentTimeZone = timeZoneOptions.some(({ value }) => value === event.timeZone); const selectableTimeZones = hasCurrentTimeZone ? timeZoneOptions : [{ value: event.timeZone, label: t("unsupportedOption", { timeZone: event.timeZone }) }, ...timeZoneOptions]; const targetValue = formatWallDateTime(event.targetUtc, event.timeZone); const startValue = event.startUtc ? formatWallDateTime(event.startUtc, event.timeZone) : null; const changeTimeZone = (timeZone: string) => { const target = resolveWallDateTimeString(targetValue, timeZone); const start = startValue ? resolveWallDateTimeString(startValue, timeZone) : null; onChange({ ...event, timeZone, targetUtc: target?.toISOString() ?? event.targetUtc, startUtc: event.startUtc ? (start?.toISOString() ?? event.startUtc) : undefined, }); }; const updateWallDateTime = (value: string | null, field: "targetUtc" | "startUtc") => { if (!value) return; const resolved = resolveWallDateTimeString(value, event.timeZone); if (!resolved) return; onChange({ ...event, [field]: resolved.toISOString() }); }; return ( onChange({ ...event, label: changeEvent.currentTarget.value })} /> ({ value, label: t(`recurrenceOption.${value}`) }))} onChange={(value) => { if (value) onChange({ ...event, recurrence: value as DateTimeEventRecurrence }); }} /> updateWallDateTime(value, "targetUtc")} confirmLabel={t("confirmDateTime")} /> {event.recurrence === "none" && ( <> { if (!changeEvent.currentTarget.checked) { const nextEvent = { ...event }; delete nextEvent.startUtc; onChange(nextEvent); return; } onChange({ ...event, startUtc: new Date().toISOString() }); }} /> {event.startUtc && ( updateWallDateTime(value, "startUtc")} confirmLabel={t("confirmDateTime")} /> )} )} ); }; interface ConfirmedDateTimePickerProps { label: string; value: string | null; confirmLabel: string; onChange: (value: string | null) => void; } const ConfirmedDateTimePicker = ({ label, value, confirmLabel, onChange }: ConfirmedDateTimePickerProps) => { const [draftValue, setDraftValue] = useState(value); useEffect(() => setDraftValue(value), [value]); const resetDraft = () => setDraftValue(value); const confirmDraft = () => { if (!draftValue) return; onChange(draftValue); }; return ( ); }; type EventListTranslationKey = | "newEvent" | "add" | "remove" | "reorder" | "eventLabel" | "timezone" | "unsupportedOption" | "recurrence" | "recurrenceOption.none" | "recurrenceOption.yearly" | "target" | "trackProgress" | "start" | "confirmDateTime" | "limit"; type EventListTranslation = (key: EventListTranslationKey, values?: Record) => string; const useEventListTranslation = (kind: WidgetKind) => useI18n(`widget.${kind}.dateTimeEventList` as never) as unknown as EventListTranslation;