diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json
index 03413b19d..7324a4d1a 100644
--- a/public/intl/messages/en-GB.json
+++ b/public/intl/messages/en-GB.json
@@ -443,6 +443,7 @@
"incorrect-username-password": "Incorrect username/password.",
"invalid-domain": "Invalid domain",
"invalid-value": "Invalid value",
+ "max-length": "Maximum length of {n} characters",
"min-password-length": "Minimum length of {n} characters",
"new-version-available": "A new version of Umami {version} is available!",
"no-data-available": "No data available.",
diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json
index 34b26c98f..02a4001f6 100644
--- a/public/intl/messages/en-US.json
+++ b/public/intl/messages/en-US.json
@@ -452,6 +452,7 @@
"incorrect-username-password": "Incorrect username and/or password.",
"invalid-domain": "Invalid domain. Do not include http/https.",
"invalid-value": "Invalid value",
+ "max-length": "Maximum length of {n} characters",
"min-password-length": "Minimum length of {n} characters",
"new-version-available": "A new version of Umami {version} is available!",
"no-data-available": "No data available.",
diff --git a/src/app/(main)/TopNav.tsx b/src/app/(main)/TopNav.tsx
index 0c253842a..829d63444 100644
--- a/src/app/(main)/TopNav.tsx
+++ b/src/app/(main)/TopNav.tsx
@@ -46,6 +46,7 @@ export function TopNav() {
paddingRight="5"
width="100%"
zIndex={100}
+ backgroundColor="surface-raised"
>
@@ -101,6 +102,17 @@ export function TopNav() {
>
)}
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
index 10564f45a..56b45482d 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
@@ -1,3 +1,4 @@
+import { isSameDay } from 'date-fns';
import { useCallback, useMemo } from 'react';
import type { ChartAnnotation } from '@/components/charts/ChartAnnotationMarkers';
import { LoadingPanel } from '@/components/common/LoadingPanel';
@@ -10,30 +11,37 @@ import {
} from '@/components/hooks';
import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery';
import { PageviewsChart } from '@/components/metrics/PageviewsChart';
-import { getAnnotationDateRangeValue } from '@/lib/annotations';
+import { type AnnotationRange, getAnnotationDateRangeValue } from '@/lib/annotations';
+import { DATE_FUNCTIONS } from '@/lib/date';
export function WebsiteChart({
websiteId,
compareMode,
+ showAnnotations,
+ onAnnotationMoreClick,
}: {
websiteId: string;
compareMode?: boolean;
+ showAnnotations?: boolean;
+ onAnnotationMoreClick?: (range: AnnotationRange) => void;
}) {
- const { timezone, localFromUtc } = useTimezone();
+ const { timezone, localFromUtc, localToUtc } = useTimezone();
const { dateRange, dateCompare } = useDateRange({ timezone: timezone });
const { startDate, endDate, unit, value } = dateRange;
const { startAt, endAt } = useDateParameters();
const { router, updateParams } = useNavigation();
- const { data: annotationData } = useWebsiteAnnotationsQuery(websiteId, {
- startAt,
- endAt,
- pageSize: 1000,
- });
+ const { data: annotationData } = useWebsiteAnnotationsQuery(
+ websiteId,
+ { startAt, endAt, pageSize: 1000 },
+ { enabled: !!showAnnotations && !!websiteId },
+ );
const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
websiteId,
compare: compareMode ? dateCompare?.compare : undefined,
});
const { pageviews, sessions, compare } = (data || {}) as any;
+ const canDrillIntoAnnotation =
+ unit !== 'hour' && unit !== 'minute' && !isSameDay(startDate, endDate);
const chartData = useMemo(() => {
if (!data) {
@@ -61,19 +69,47 @@ export function WebsiteChart({
}, [data, startDate, endDate, unit]);
const annotations = useMemo(() => {
- return (annotationData?.data || []).map(({ id, date, note, allDay }) => ({
- id,
- date: localFromUtc(new Date(date)),
- label: note,
- allDay,
- }));
- }, [annotationData, timezone]);
+ const isSubDayUnit = unit === 'hour' || unit === 'minute';
+
+ return (annotationData?.data || [])
+ .filter(({ allDay }) => !isSubDayUnit || allDay === false)
+ .map(({ id, date, note, allDay }) => {
+ const annotationDate = localFromUtc(new Date(date));
+
+ return {
+ id,
+ date: annotationDate,
+ markerDate: DATE_FUNCTIONS[unit].start(annotationDate),
+ label: note,
+ allDay,
+ isClickable: canDrillIntoAnnotation,
+ isGroupClickable: unit === 'month',
+ };
+ });
+ }, [annotationData, timezone, unit, canDrillIntoAnnotation]);
const handleAnnotationClick = useCallback(
- (annotation: ChartAnnotation) => {
+ (annotations: ChartAnnotation[]) => {
+ const [annotation] = annotations;
+ const hasOneDate = annotations.every(item => isSameDay(item.date, annotation.date));
+
+ if (hasOneDate) {
+ router.push(
+ updateParams({
+ date: getAnnotationDateRangeValue(annotation.date, annotation.allDay !== false),
+ offset: undefined,
+ }),
+ );
+ return;
+ }
+
+ const markerDate = annotation.markerDate || annotation.date;
+ const startDate = DATE_FUNCTIONS.month.start(markerDate);
+ const endDate = DATE_FUNCTIONS.month.end(markerDate);
+
router.push(
updateParams({
- date: getAnnotationDateRangeValue(annotation.date, annotation.allDay !== false),
+ date: `range:${startDate.getTime()}:${endDate.getTime()}`,
offset: undefined,
}),
);
@@ -81,6 +117,19 @@ export function WebsiteChart({
[router, updateParams],
);
+ const handleAnnotationMoreClick = useCallback(
+ (annotations: ChartAnnotation[]) => {
+ const markerDate = annotations[0].markerDate || annotations[0].date;
+ const { start, end } = DATE_FUNCTIONS[unit];
+
+ onAnnotationMoreClick?.({
+ startAt: +localToUtc(start(markerDate)),
+ endAt: +localToUtc(end(markerDate)),
+ });
+ },
+ [localToUtc, onAnnotationMoreClick, unit],
+ );
+
return (
);
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
index 74a09acc3..710297c20 100644
--- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
@@ -1,15 +1,21 @@
'use client';
import { Column, Row } from '@umami/react-zen';
+import { useState } from 'react';
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
import { Panel } from '@/components/common/Panel';
+import { DialogButton } from '@/components/input/DialogButton';
+import type { AnnotationRange } from '@/lib/annotations';
import { UnitFilter } from '@/components/input/UnitFilter';
import { AnnotationsButton } from './annotations/AnnotationsButton';
+import { AnnotationsModal } from './annotations/AnnotationsModal';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteControls } from './WebsiteControls';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { WebsitePanels } from './WebsitePanels';
export function WebsitePage({ websiteId }: { websiteId: string }) {
+ const [annotationRange, setAnnotationRange] = useState(null);
+
return (
@@ -18,13 +24,36 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
-
+
+ !isOpen && setAnnotationRange(null)}
+ title={null}
+ width="800px"
+ >
+ {({ close }) =>
+ annotationRange && (
+ {
+ close();
+ setAnnotationRange(null);
+ }}
+ />
+ )
+ }
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/annotations/AnnotationDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/annotations/AnnotationDeleteButton.tsx
index 02aacb6c1..84afaad35 100644
--- a/src/app/(main)/websites/[websiteId]/annotations/AnnotationDeleteButton.tsx
+++ b/src/app/(main)/websites/[websiteId]/annotations/AnnotationDeleteButton.tsx
@@ -6,12 +6,10 @@ import { DialogButton } from '@/components/input/DialogButton';
export function AnnotationDeleteButton({
annotationId,
websiteId,
- name,
onSave,
}: {
annotationId: string;
websiteId: string;
- name: string;
onSave?: () => void;
}) {
const { t, labels, messages } = useMessages();
@@ -33,10 +31,7 @@ export function AnnotationDeleteButton({
} variant="quiet" title={t(labels.confirm)} width="400px">
{({ close }) => (
{chunks},
- })}
+ message={t(messages.confirmDelete, { target: t(labels.note) })}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
diff --git a/src/app/(main)/websites/[websiteId]/annotations/AnnotationEditForm.tsx b/src/app/(main)/websites/[websiteId]/annotations/AnnotationEditForm.tsx
index 7df122748..7dc80bcef 100644
--- a/src/app/(main)/websites/[websiteId]/annotations/AnnotationEditForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/annotations/AnnotationEditForm.tsx
@@ -1,17 +1,23 @@
import {
Button,
+ Calendar as ZenCalendar,
Column,
- DatePicker,
+ DialogTrigger,
Form,
FormButtons,
FormField,
FormSubmitButton,
+ Icon,
Label,
+ Popover,
+ Text,
TextField,
} from '@umami/react-zen';
import { startOfDay } from 'date-fns';
import { useMemo, useState } from 'react';
import { useLocale, useMessages, useTimezone, useUpdateQuery } from '@/components/hooks';
+import { Calendar } from '@/components/icons';
+import { formatDate } from '@/lib/date';
export interface Annotation {
id: string;
@@ -21,6 +27,51 @@ export interface Annotation {
note: string;
}
+function AnnotationDatePicker({
+ date,
+ label,
+ locale,
+ onChange,
+}: {
+ date: Date;
+ label: string;
+ locale: string;
+ onChange: (date: Date) => void;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleChange = (value: Date) => {
+ onChange(value);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
export function AnnotationEditForm({
annotation,
websiteId,
@@ -70,29 +121,51 @@ export function AnnotationEditForm({
onSubmit={handleSubmit}
defaultValues={{ note: annotation?.note || '' }}
>
-
-
-
- value && setDate(value)} />
-
+ {({ watch }) => {
+ const noteLength = (watch('note') || '').length;
+ const isOverLimit = noteLength > 500;
-
-
-
+ return (
+
+
-
-
-
- {t(labels.save)}
-
-
-
+
+
+
+
+
+ {noteLength} / 500
+
+
+
+
+
+
+ {t(labels.save)}
+
+
+
+ );
+ }}
);
}
diff --git a/src/app/(main)/websites/[websiteId]/annotations/AnnotationsButton.tsx b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsButton.tsx
index 9d04d5617..fa9546736 100644
--- a/src/app/(main)/websites/[websiteId]/annotations/AnnotationsButton.tsx
+++ b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsButton.tsx
@@ -1,15 +1,11 @@
-import { useMessages, useShare } from '@/components/hooks';
+import { useDateParameters, useMessages } from '@/components/hooks';
import { MessageSquareText } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { AnnotationsModal } from './AnnotationsModal';
export function AnnotationsButton({ websiteId }: { websiteId: string }) {
const { t, labels } = useMessages();
- const share = useShare();
-
- if (share) {
- return null;
- }
+ const { startAt, endAt } = useDateParameters();
return (
- {({ close }) => }
+ {({ close }) => (
+
+ )}
);
}
diff --git a/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.test.tsx b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.test.tsx
new file mode 100644
index 000000000..25661ce56
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.test.tsx
@@ -0,0 +1,66 @@
+import { beforeEach, expect, test, vi } from 'vitest';
+import { render, screen } from '@/test/render';
+import { AnnotationsModal } from './AnnotationsModal';
+
+const mockUseWebsiteAnnotationsQuery = vi.fn();
+
+vi.mock('@/components/charts/ChartAnnotationMarkers', () => ({
+ formatAnnotationDate: () => '',
+}));
+
+vi.mock('@/components/hooks', () => ({
+ useLocale: () => ({ locale: 'en-US' }),
+ useMessages: () => ({
+ t: (value: string) => value,
+ labels: {
+ addNote: 'Add note',
+ all: 'All',
+ current: 'Current',
+ date: 'Date',
+ dateRange: 'date range',
+ note: 'Note',
+ notes: 'Notes',
+ },
+ messages: {
+ noDataAvailable: 'No data available',
+ },
+ }),
+ useMobile: () => ({ isMobile: false }),
+ useNavigation: () => ({ router: { push: vi.fn() }, updateParams: vi.fn() }),
+ useShare: () => null,
+ useTimezone: () => ({ localFromUtc: (date: Date) => date }),
+ useWebsiteAnnotationsQuery: (...args: unknown[]) => mockUseWebsiteAnnotationsQuery(...args),
+}));
+
+vi.mock('@/components/input/FilterButtons', () => ({
+ FilterButtons: ({ onChange }: { onChange: (value: string) => void }) => (
+
+ ),
+}));
+
+beforeEach(() => {
+ mockUseWebsiteAnnotationsQuery.mockClear();
+ mockUseWebsiteAnnotationsQuery.mockReturnValue({ data: { data: [], count: 0 }, isLoading: false });
+});
+
+test('defaults to the supplied date range and can show all notes', async () => {
+ const { user } = render(
+ ,
+ );
+
+ expect(mockUseWebsiteAnnotationsQuery).toHaveBeenLastCalledWith('website-1', {
+ page: 1,
+ pageSize: 20,
+ startAt: 1000,
+ endAt: 2000,
+ });
+
+ await user.click(screen.getByRole('button', { name: 'All notes' }));
+
+ expect(mockUseWebsiteAnnotationsQuery).toHaveBeenLastCalledWith('website-1', {
+ page: 1,
+ pageSize: 20,
+ });
+});
diff --git a/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.tsx b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.tsx
index 62c02b7c1..d39b1c2b1 100644
--- a/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.tsx
+++ b/src/app/(main)/websites/[websiteId]/annotations/AnnotationsModal.tsx
@@ -13,44 +13,61 @@ import { formatAnnotationDate } from '@/components/charts/ChartAnnotationMarkers
import { Empty } from '@/components/common/Empty';
import { IconLabel } from '@/components/common/IconLabel';
import { Pager } from '@/components/common/Pager';
+import { FilterButtons } from '@/components/input/FilterButtons';
import {
useLocale,
useMessages,
+ useMobile,
useNavigation,
+ useShare,
useTimezone,
useWebsiteAnnotationsQuery,
} from '@/components/hooks';
import { Edit, Plus } from '@/components/icons';
-import { getAnnotationDateRangeValue } from '@/lib/annotations';
+import { type AnnotationRange, getAnnotationDateRangeValue } from '@/lib/annotations';
import { AnnotationDeleteButton } from './AnnotationDeleteButton';
import { type Annotation, AnnotationEditForm } from './AnnotationEditForm';
const PAGE_SIZE = 20;
+const RANGE_FILTER = 'range';
+const ALL_FILTER = 'all';
+
type View = { mode: 'list' } | { mode: 'add' } | { mode: 'edit'; annotation: Annotation };
export function AnnotationsModal({
websiteId,
+ range,
onClose,
}: {
websiteId: string;
+ range: AnnotationRange;
onClose?: () => void;
}) {
const { t, labels } = useMessages();
const { locale } = useLocale();
const { localFromUtc } = useTimezone();
+ const { isMobile } = useMobile();
+ const share = useShare();
const { router, updateParams } = useNavigation();
const [view, setView] = useState({ mode: 'list' });
const [page, setPage] = useState(1);
+ const [filter, setFilter] = useState(RANGE_FILTER);
const { data, isLoading } = useWebsiteAnnotationsQuery(websiteId, {
page,
pageSize: PAGE_SIZE,
+ ...(filter === RANGE_FILTER && range),
});
const showList = () => setView({ mode: 'list' });
const toLocalDate = (annotation: Annotation) => localFromUtc(new Date(annotation.date));
+ const handleFilterChange = (value: string) => {
+ setFilter(value);
+ setPage(1);
+ };
+
const handleSelect = (annotation: Annotation) => {
router.push(
updateParams({
@@ -85,15 +102,25 @@ export function AnnotationsModal({
{t(labels.notes)}
-
+ {!share && (
+
+ )}
+
{rows.length === 0 ? (
) : (
-
+
{(row: Annotation) => (
)}
{data?.count > PAGE_SIZE && (
)}
+ {onClose && (
+
+ {t(labels.close)}
+
+ )}
);
}
diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx
index bf86cfe71..a68a575b0 100644
--- a/src/components/charts/Chart.tsx
+++ b/src/components/charts/Chart.tsx
@@ -12,6 +12,7 @@ import {
ChartAnnotationMarkers,
} from '@/components/charts/ChartAnnotationMarkers';
import { Legend } from '@/components/metrics/Legend';
+import { getChartBucketIndex } from '@/lib/charts';
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
ChartJS.defaults.font.family = 'Inter';
@@ -26,7 +27,8 @@ export interface ChartProps extends BoxProps {
hiddenLabels?: Set;
onLegendClick?: (label: string, willBeHidden: boolean) => void;
annotations?: ChartAnnotation[];
- onAnnotationClick?: (annotation: ChartAnnotation) => void;
+ onAnnotationClick?: (annotations: ChartAnnotation[]) => void;
+ onAnnotationMoreClick?: (annotations: ChartAnnotation[]) => void;
}
function isSameMarkers(a: AnnotationMarker[], b: AnnotationMarker[]) {
@@ -53,6 +55,7 @@ export function Chart({
onLegendClick,
annotations,
onAnnotationClick,
+ onAnnotationMoreClick,
...props
}: ChartProps) {
const canvas = useRef(null);
@@ -79,7 +82,13 @@ export function Chart({
const groups = new Map();
for (const annotation of list) {
- const x = Math.round(scales.x.getPixelForValue(annotation.date.getTime()));
+ const markerDate = annotation.markerDate || annotation.date;
+ const bucketIndex = getChartBucketIndex(
+ instance.data.datasets[0]?.data || [],
+ markerDate,
+ );
+ const bucket = instance.getDatasetMeta(0)?.data[bucketIndex];
+ const x = Math.round(bucket?.x ?? scales.x.getPixelForValue(markerDate.getTime()));
if (!Number.isFinite(x) || x < chartArea.left || x > chartArea.right) {
continue;
@@ -217,7 +226,11 @@ export function Chart({
{markers.length > 0 && (
-
+
)}
diff --git a/src/components/charts/ChartAnnotationMarkers.module.css b/src/components/charts/ChartAnnotationMarkers.module.css
index 62ea86cc1..d9b2c3749 100644
--- a/src/components/charts/ChartAnnotationMarkers.module.css
+++ b/src/components/charts/ChartAnnotationMarkers.module.css
@@ -37,6 +37,34 @@
.note {
max-width: 300px;
+ max-height: min(280px, 50vh);
+ overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
+
+.noteFull {
+ display: block;
+}
+
+.noteText {
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+}
+
+.moreButton {
+ align-self: center;
+ min-height: 0;
+ padding: 2px 4px;
+ border-radius: 4px;
+ color: inherit;
+ opacity: 0.8;
+}
+
+.moreButton:hover,
+.moreButton:focus-visible {
+ background-color: rgba(255, 255, 255, 0.15);
+ opacity: 1;
+}
diff --git a/src/components/charts/ChartAnnotationMarkers.tsx b/src/components/charts/ChartAnnotationMarkers.tsx
index 4c02435da..0393c49fb 100644
--- a/src/components/charts/ChartAnnotationMarkers.tsx
+++ b/src/components/charts/ChartAnnotationMarkers.tsx
@@ -1,15 +1,23 @@
-import { Column, Text, Tooltip, TooltipTrigger, useTheme } from '@umami/react-zen';
-import { useMemo } from 'react';
-import { useLocale } from '@/components/hooks';
+import { IconLabel } from '@/components/common/IconLabel';
+import { useLocale, useMessages } from '@/components/hooks';
+import { Maximize } from '@/components/icons';
import { getThemeColors } from '@/lib/colors';
import { formatDate } from '@/lib/date';
+import { Button, Column, Text, Tooltip, TooltipTrigger, useTheme } from '@umami/react-zen';
+import { isSameDay } from 'date-fns';
+import { useMemo } from 'react';
import styles from './ChartAnnotationMarkers.module.css';
+const MAX_TOOLTIP_ANNOTATIONS = 3;
+
export interface ChartAnnotation {
id: string;
date: Date;
+ markerDate?: Date;
label: string;
allDay?: boolean;
+ isClickable?: boolean;
+ isGroupClickable?: boolean;
}
export interface AnnotationMarker {
@@ -25,40 +33,80 @@ export function formatAnnotationDate(annotation: ChartAnnotation, locale: string
export function ChartAnnotationMarkers({
markers,
onClick,
+ onMoreClick,
}: {
markers: AnnotationMarker[];
- onClick?: (annotation: ChartAnnotation) => void;
+ onClick?: (annotations: ChartAnnotation[]) => void;
+ onMoreClick?: (annotations: ChartAnnotation[]) => void;
}) {
const { theme } = useTheme();
const { locale } = useLocale();
+ const { t, labels } = useMessages();
const { colors } = useMemo(() => getThemeColors(theme), [theme]);
return (
<>
{markers.map(({ annotations, x, y }) => {
const [first] = annotations;
+ const hasOneDate = annotations.every(annotation => isSameDay(annotation.date, first.date));
+ const isClickable =
+ !!onClick &&
+ (hasOneDate
+ ? first.isClickable !== false
+ : annotations.every(annotation => annotation.isGroupClickable === true));
+ const visibleAnnotations = annotations.slice(0, MAX_TOOLTIP_ANNOTATIONS);
return (
label).join(', ')}
- onClick={() => onClick?.(first)}
+ onClick={isClickable ? () => onClick?.(annotations) : undefined}
>
{annotations.length > 1 && {annotations.length}}
-
- {annotations.map(annotation => (
+
+ {visibleAnnotations.map(annotation => (
{formatAnnotationDate(annotation, locale)}
- {annotation.label}
+
+ {annotation.label}
+
))}
+ {annotations.length > 1 &&
+ (onMoreClick ? (
+ onMoreClick(annotations)}
+ >
+ }>{t(labels.view)}
+
+ ) : (
+ } style={{ opacity: 0.8 }}>
+ {t(labels.view)}
+
+ ))}
diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx
index fd7663369..7f658862e 100644
--- a/src/components/common/ConfirmationForm.tsx
+++ b/src/components/common/ConfirmationForm.tsx
@@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
import { useMessages } from '@/components/hooks';
export interface ConfirmationFormProps {
- message: ReactNode;
+ message?: ReactNode;
buttonLabel?: ReactNode;
buttonVariant?: 'primary' | 'quiet' | 'danger';
isLoading?: boolean;
@@ -25,7 +25,7 @@ export function ConfirmationForm({
return (