diff --git a/src/components/icons/IconCart.tsx b/src/components/icons/IconCart.tsx
index 04c74385453..709990c37f5 100644
--- a/src/components/icons/IconCart.tsx
+++ b/src/components/icons/IconCart.tsx
@@ -27,79 +27,78 @@ const IconCart = ({
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/src/components/time_picker/index.tsx b/src/components/time_picker/index.tsx
index c667165e1c7..e81f5494b41 100644
--- a/src/components/time_picker/index.tsx
+++ b/src/components/time_picker/index.tsx
@@ -24,6 +24,8 @@ const TimePicker = ({
onChange,
sx,
readOnly = false,
+ error,
+ helperText,
}: CustomTimePickerProps) => {
const divRef = useRef(null);
@@ -91,6 +93,8 @@ const TimePicker = ({
label: label,
value: valueTmp,
onClick: () => setOpen(!open),
+ error,
+ helperText,
},
toolbar: {
hidden: false,
diff --git a/src/components/time_picker/index.types.ts b/src/components/time_picker/index.types.ts
index 84bb4cd34d8..da55928d3cb 100644
--- a/src/components/time_picker/index.types.ts
+++ b/src/components/time_picker/index.types.ts
@@ -22,4 +22,6 @@ export interface CustomTimePickerProps {
onChange?: (value: Date) => void;
sx?: SxProps;
readOnly?: boolean;
+ error?: boolean;
+ helperText?: string;
}
diff --git a/src/constants/table_encryption_map.ts b/src/constants/table_encryption_map.ts
index 2ff0597d3e3..1769161f2ca 100644
--- a/src/constants/table_encryption_map.ts
+++ b/src/constants/table_encryption_map.ts
@@ -59,6 +59,7 @@ export const TABLE_ENCRYPTION_MAP = {
circuit_overseer: 'shared',
language_groups: 'shared',
format_24h_enabled: 'shared',
+ events_multiday_display: 'shared',
week_start_sunday: 'shared',
attendance_online_record: 'shared',
data_sync: 'public',
diff --git a/src/definition/settings.ts b/src/definition/settings.ts
index 0cb849da469..f69ec3085b9 100644
--- a/src/definition/settings.ts
+++ b/src/definition/settings.ts
@@ -1,5 +1,6 @@
import { AppRoleType } from './app';
import { AssignmentFieldType } from './assignment';
+import { UpcomingEventDisplayType } from './upcoming_events';
export enum FullnameOption {
FIRST_BEFORE_LAST = 1,
@@ -156,6 +157,12 @@ export type SettingsType = {
updatedAt: string;
_deleted: boolean;
}[];
+ events_multiday_display: {
+ type: string;
+ value: UpcomingEventDisplayType;
+ updatedAt: string;
+ _deleted: boolean;
+ }[];
week_start_sunday: {
type: string;
value: boolean;
diff --git a/src/definition/upcoming_events.ts b/src/definition/upcoming_events.ts
index 593e2b3254f..830d1b44113 100644
--- a/src/definition/upcoming_events.ts
+++ b/src/definition/upcoming_events.ts
@@ -6,8 +6,8 @@ export enum UpcomingEventCategory {
AssemblyWeek,
InternationalConventionWeek,
SpecialCampaignWeek,
- TheocraticTrainingWeek,
HallMaintenanceTrainingWeek,
+ TheocraticTrainingWeek,
BethelTour,
SpecialProgram,
PublicWitnessing,
@@ -22,6 +22,12 @@ export enum UpcomingEventDuration {
MultipleDays,
}
+export type UpcomingEventDisplayType = 'byDay' | 'range';
+
+export const UPCOMING_EVENT_MAX_LIST_DAYS = 7;
+
+export const DEFAULT_EVENT_START_HOUR = 9;
+
export type UpcomingEventType = {
event_uid: string;
_deleted?: boolean;
@@ -36,6 +42,7 @@ export type UpcomingEventType = {
duration: UpcomingEventDuration;
description: string;
custom?: string;
+ wholeDay?: boolean;
};
};
@@ -48,6 +55,8 @@ export type UpcomingEventDataType = {
description: string;
category: UpcomingEventCategory;
duration: UpcomingEventDuration;
+ wholeDay: boolean;
+ showAsRange: boolean;
start: string;
date: string;
day: string;
diff --git a/src/features/activities/upcoming_events/add_to_calendar/useAddToCalendar.tsx b/src/features/activities/upcoming_events/add_to_calendar/useAddToCalendar.tsx
index 7274672aa6b..48fde8ad7e5 100644
--- a/src/features/activities/upcoming_events/add_to_calendar/useAddToCalendar.tsx
+++ b/src/features/activities/upcoming_events/add_to_calendar/useAddToCalendar.tsx
@@ -1,10 +1,8 @@
import { useState } from 'react';
import { createEvent, EventAttributes } from 'ics';
import { AddToCalendarProps } from './index.types';
-import {
- UpcomingEventCategory,
- UpcomingEventDuration,
-} from '@definition/upcoming_events';
+import { UpcomingEventCategory } from '@definition/upcoming_events';
+import { isWholeDayEvent } from '@services/app/upcoming_events';
import { useAppTranslation } from '@hooks/index';
import { decorationsForEvent } from '../decorations_for_event';
import { displaySnackNotification } from '@services/states/app';
@@ -28,6 +26,7 @@ const useAddToCalendar = ({ event }: AddToCalendarProps) => {
const startDate = new Date(event.event_data.start);
const endDate = new Date(event.event_data.end);
+ const wholeDay = isWholeDayEvent(event);
const eventTitle =
event.event_data.category !== UpcomingEventCategory.Custom
@@ -37,34 +36,33 @@ const useAddToCalendar = ({ event }: AddToCalendarProps) => {
const eventDetails: EventAttributes = {
title: eventTitle,
description: event.event_data.description,
- start:
- event.event_data.duration === UpcomingEventDuration.SingleDay
- ? [
- startDate.getFullYear(),
- startDate.getMonth() + 1,
- startDate.getDate(),
- startDate.getHours(),
- startDate.getMinutes(),
- ]
- : [
- startDate.getFullYear(),
- startDate.getMonth() + 1,
- startDate.getDate(),
- ],
- end:
- event.event_data.duration === UpcomingEventDuration.SingleDay
- ? [
- endDate.getFullYear(),
- endDate.getMonth() + 1,
- endDate.getDate(),
- endDate.getHours(),
- endDate.getMinutes(),
- ]
- : [
- endDate.getFullYear(),
- endDate.getMonth() + 1,
- endDate.getDate() + 1,
- ],
+ start: wholeDay
+ ? [
+ startDate.getFullYear(),
+ startDate.getMonth() + 1,
+ startDate.getDate(),
+ ]
+ : [
+ startDate.getFullYear(),
+ startDate.getMonth() + 1,
+ startDate.getDate(),
+ startDate.getHours(),
+ startDate.getMinutes(),
+ ],
+ end: wholeDay
+ ? [
+ endDate.getFullYear(),
+ endDate.getMonth() + 1,
+ // all-day ranges are exclusive of the end date
+ endDate.getDate() + 1,
+ ]
+ : [
+ endDate.getFullYear(),
+ endDate.getMonth() + 1,
+ endDate.getDate(),
+ endDate.getHours(),
+ endDate.getMinutes(),
+ ],
};
createEvent(eventDetails, (error, value) => {
diff --git a/src/features/activities/upcoming_events/decorations_for_event.tsx b/src/features/activities/upcoming_events/decorations_for_event.tsx
index 8230532610b..ef419b9be12 100644
--- a/src/features/activities/upcoming_events/decorations_for_event.tsx
+++ b/src/features/activities/upcoming_events/decorations_for_event.tsx
@@ -18,85 +18,102 @@ import {
} from '@components/icons';
import { UpcomingEventDuration } from '@definition/upcoming_events';
+// the index of each entry is the stored `category` value — never reorder
export const decorationsForEvent = [
{
translationKey: 'tr_circuitOverseerWeek',
icon: ,
duration: UpcomingEventDuration.MultipleDays,
+ wholeDay: true,
},
{
translationKey: 'tr_pioneerWeek',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_memorialWeek',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_conventionWeek',
icon: ,
duration: UpcomingEventDuration.MultipleDays,
+ wholeDay: true,
},
{
translationKey: 'tr_assemblyWeek',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: true,
},
{
translationKey: 'tr_internationalConventionWeek',
icon: ,
duration: UpcomingEventDuration.MultipleDays,
+ wholeDay: true,
},
{
translationKey: 'tr_specialCampaignWeek',
icon: ,
duration: UpcomingEventDuration.MultipleDays,
+ wholeDay: true,
},
{
translationKey: 'tr_hallMaintenanceTrainingWeek',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_theocraticTrainingWeek',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_bethelTour',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: true,
},
{
translationKey: 'tr_specialProgram',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_publicWitnessing',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_kingdomDedication',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_languageCourse',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_annualMeeting',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
{
translationKey: 'tr_custom',
icon: ,
duration: UpcomingEventDuration.SingleDay,
+ wholeDay: false,
},
];
diff --git a/src/features/activities/upcoming_events/delete_event/index.tsx b/src/features/activities/upcoming_events/delete_event/index.tsx
new file mode 100644
index 00000000000..b2af4abfa5c
--- /dev/null
+++ b/src/features/activities/upcoming_events/delete_event/index.tsx
@@ -0,0 +1,33 @@
+import { Stack } from '@mui/material';
+import { useAppTranslation } from '@hooks/index';
+import { DeleteEventProps } from './index.types';
+import Button from '@components/button';
+import Dialog from '@components/dialog';
+import Typography from '@components/typography';
+
+const DeleteEvent = ({ open, title, onClose, onConfirm }: DeleteEventProps) => {
+ const { t } = useAppTranslation();
+
+ return (
+
+ );
+};
+
+export default DeleteEvent;
diff --git a/src/features/activities/upcoming_events/delete_event/index.types.ts b/src/features/activities/upcoming_events/delete_event/index.types.ts
new file mode 100644
index 00000000000..813507f6ed5
--- /dev/null
+++ b/src/features/activities/upcoming_events/delete_event/index.types.ts
@@ -0,0 +1,6 @@
+export type DeleteEventProps = {
+ open: boolean;
+ title: string;
+ onClose: VoidFunction;
+ onConfirm: VoidFunction;
+};
diff --git a/src/features/activities/upcoming_events/edit_upcoming_event/index.tsx b/src/features/activities/upcoming_events/edit_upcoming_event/index.tsx
index 07d297222de..cd13fddeb5f 100644
--- a/src/features/activities/upcoming_events/edit_upcoming_event/index.tsx
+++ b/src/features/activities/upcoming_events/edit_upcoming_event/index.tsx
@@ -11,10 +11,12 @@ import { EditUpcomingEventProps } from './index.types';
import useEditUpcomingEvent from './useEditUpcomingEvent';
import Button from '@components/button';
import DatePicker from '@components/date_picker';
+import DeleteEvent from '../delete_event';
import Divider from '@components/divider';
import IconButton from '@components/icon_button';
import MenuItem from '@components/menuitem';
import Select from '@components/select';
+import SwitchWithLabel from '@components/switch_with_label';
import TextField from '@components/textfield';
import TimePicker from '@components/time_picker';
import Typography from '@components/typography';
@@ -38,6 +40,12 @@ const EditUpcomingEvent = (props: EditUpcomingEventProps) => {
handleChangeEventStartTime,
handleChangeEventEndDate,
handleChangeEventEndTime,
+ wholeDay,
+ handleToggleWholeDay,
+ deleteOpen,
+ handleOpenDeleteConfirm,
+ handleCloseDeleteConfirm,
+ eventTitle,
} = useEditUpcomingEvent(props);
return (
@@ -52,6 +60,15 @@ const EditUpcomingEvent = (props: EditUpcomingEventProps) => {
backgroundColor: 'var(--white)',
}}
>
+ {deleteOpen && (
+
+ )}
+
{
{props.type === 'edit' && !tabletUp && (
-
+
)}
@@ -81,7 +98,7 @@ const EditUpcomingEvent = (props: EditUpcomingEventProps) => {
height: '32px',
minHeight: '32px !important',
}}
- onClick={handleDeleteEvent}
+ onClick={handleOpenDeleteConfirm}
>
{t('tr_delete')}
@@ -158,6 +175,7 @@ const EditUpcomingEvent = (props: EditUpcomingEventProps) => {
gap: '16px',
flexWrap: !desktopUp ? 'wrap' : 'nowrap',
+ '& > *': { flex: !desktopUp ? 'none' : '1' },
}}
>
+
+ {localEvent.event_data.duration === UpcomingEventDuration.SingleDay && (
+ <>
+
+
+ {!wholeDay && (
+ *': { flex: !desktopUp ? 'none' : '1' },
+ }}
+ >
+
+
+
+ )}
+ >
+ )}
diff --git a/src/features/activities/upcoming_events/edit_upcoming_event/useEditUpcomingEvent.tsx b/src/features/activities/upcoming_events/edit_upcoming_event/useEditUpcomingEvent.tsx
index 03c906e07d2..ee1ed913faa 100644
--- a/src/features/activities/upcoming_events/edit_upcoming_event/useEditUpcomingEvent.tsx
+++ b/src/features/activities/upcoming_events/edit_upcoming_event/useEditUpcomingEvent.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, useCallback, useState } from 'react';
+import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { SelectChangeEvent } from '@mui/material';
import {
@@ -6,23 +6,46 @@ import {
UpcomingEventDuration,
} from '@definition/upcoming_events';
import { hour24FormatState } from '@states/settings';
-import { stackDatesToOne } from '@utils/date';
+import { isWholeDayEvent } from '@services/app/upcoming_events';
+import {
+ addHours,
+ formatDate,
+ stackDatesToOne,
+ sundayOfWeek,
+} from '@utils/date';
import { decorationsForEvent } from '../decorations_for_event';
import { EditUpcomingEventProps } from './index.types';
+import { useAppTranslation } from '@hooks/index';
const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
+ const { t } = useAppTranslation();
+
const hour24 = useAtomValue(hour24FormatState);
const [localEvent, setLocalEvent] = useState(data);
const [wasSubmitted, setWasSubmitted] = useState(false);
+ const [deleteOpen, setDeleteOpen] = useState(false);
const [errors, setErrors] = useState({
category: false,
duration: false,
custom: false,
+ endTime: false,
});
+ const wholeDay = useMemo(() => isWholeDayEvent(localEvent), [localEvent]);
+
+ const eventTitle = useMemo(() => {
+ const { category, custom } = localEvent.event_data;
+
+ if (category === UpcomingEventCategory.Custom) return custom ?? '';
+
+ const decoration = decorationsForEvent[category];
+
+ return decoration ? t(decoration.translationKey) : '';
+ }, [localEvent.event_data, t]);
+
const validateField = useCallback(
(field: keyof typeof errors, value) => {
const data = localEvent.event_data;
@@ -37,11 +60,23 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
data.category === UpcomingEventCategory.Custom &&
(!value || value.trim() === '')
);
+ case 'endTime':
+ if (data.duration === UpcomingEventDuration.MultipleDays) {
+ return (
+ formatDate(new Date(data.end), 'yyyy/MM/dd') <=
+ formatDate(new Date(data.start), 'yyyy/MM/dd')
+ );
+ }
+
+ return (
+ !isWholeDayEvent(localEvent) &&
+ new Date(data.end) <= new Date(data.start)
+ );
default:
return false;
}
},
- [localEvent.event_data]
+ [localEvent]
);
const validateForm = useCallback(() => {
@@ -51,6 +86,7 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
category: validateField('category', data.category),
duration: validateField('duration', data.duration),
custom: validateField('custom', data.custom),
+ endTime: validateField('endTime', data.end),
};
setErrors(newErrors);
@@ -67,6 +103,7 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
...prev.event_data,
category: targetValue,
duration: decorationsForEvent[targetValue].duration,
+ wholeDay: decorationsForEvent[targetValue].wholeDay,
},
}));
@@ -117,53 +154,106 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
const handleChangeEventDuration = useCallback(
(event: SelectChangeEvent) => {
+ const duration = event.target.value as UpcomingEventDuration;
+
setLocalEvent((prev) => {
+ const category = prev.event_data.category;
+ const singleDay = duration === UpcomingEventDuration.SingleDay;
+
return {
...prev,
event_data: {
...prev.event_data,
- duration: event.target.value as UpcomingEventDuration,
+ duration,
+ wholeDay: singleDay
+ ? (decorationsForEvent[category]?.wholeDay ?? false)
+ : true,
+ end: singleDay
+ ? stackDatesToOne(
+ new Date(prev.event_data.start),
+ new Date(prev.event_data.end),
+ true
+ ).toISOString()
+ : prev.event_data.end,
},
};
});
if (wasSubmitted) {
- setErrors((prev) => ({ ...prev, duration: false }));
+ setErrors((prev) => ({ ...prev, duration: false, endTime: false }));
}
},
[wasSubmitted]
);
+ const handleToggleWholeDay = useCallback((value: boolean) => {
+ setLocalEvent((prev) => ({
+ ...prev,
+ event_data: { ...prev.event_data, wholeDay: value },
+ }));
+
+ setErrors((prev) => ({ ...prev, endTime: false }));
+ }, []);
+
const handleChangeEventStartDate = useCallback((value: Date) => {
setLocalEvent((prev) => {
- return {
- ...prev,
- event_data: {
- ...prev.event_data,
- start: stackDatesToOne(
- value,
- new Date(prev.event_data.start),
- true
- ).toISOString(),
- },
+ const event_data = {
+ ...prev.event_data,
+ start: stackDatesToOne(
+ value,
+ new Date(prev.event_data.start),
+ true
+ ).toISOString(),
};
+
+ if (prev.event_data.duration === UpcomingEventDuration.SingleDay) {
+ event_data.end = stackDatesToOne(
+ value,
+ new Date(prev.event_data.end),
+ true
+ ).toISOString();
+ }
+
+ if (
+ prev.event_data.category ===
+ UpcomingEventCategory.CircuitOverseerWeek &&
+ prev.event_data.duration === UpcomingEventDuration.MultipleDays
+ ) {
+ event_data.end = stackDatesToOne(
+ sundayOfWeek(value),
+ new Date(prev.event_data.end),
+ true
+ ).toISOString();
+ }
+
+ return { ...prev, event_data };
});
+
+ setErrors((prev) => ({ ...prev, endTime: false }));
}, []);
const handleChangeEventStartTime = useCallback((value: Date) => {
setLocalEvent((prev) => {
+ const start = stackDatesToOne(
+ new Date(prev.event_data.start),
+ value,
+ true
+ );
+
+ const end = new Date(prev.event_data.end);
+
return {
...prev,
event_data: {
...prev.event_data,
- start: stackDatesToOne(
- new Date(prev.event_data.start),
- value,
- true
- ).toISOString(),
+ start: start.toISOString(),
+ end:
+ end <= start ? addHours(1, start).toISOString() : end.toISOString(),
},
};
});
+
+ setErrors((prev) => ({ ...prev, endTime: false }));
}, []);
const handleChangeEventEndDate = useCallback((value: Date) => {
@@ -180,6 +270,8 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
},
};
});
+
+ setErrors((prev) => ({ ...prev, endTime: false }));
}, []);
const handleChangeEventEndTime = useCallback((value: Date) => {
@@ -196,6 +288,8 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
},
};
});
+
+ setErrors((prev) => ({ ...prev, endTime: false }));
}, []);
const handleSaveEvent = useCallback(() => {
@@ -209,12 +303,18 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
}
}, [localEvent, onSave, validateForm]);
+ const handleOpenDeleteConfirm = useCallback(() => setDeleteOpen(true), []);
+
+ const handleCloseDeleteConfirm = useCallback(() => setDeleteOpen(false), []);
+
const handleDeleteEvent = useCallback(() => {
const event = structuredClone(localEvent);
event.event_data._deleted = true;
event.event_data.updatedAt = new Date().toISOString();
+ setDeleteOpen(false);
+
onSave(event);
}, [localEvent, onSave]);
@@ -222,18 +322,24 @@ const useEditUpcomingEvent = ({ data, onSave }: EditUpcomingEventProps) => {
hour24,
localEvent,
errors,
+ wholeDay,
handleChangeEventCategory,
handleChangeEventCustomTitle,
handleChangeEventDescription,
handleChangeEventDuration,
+ handleToggleWholeDay,
handleChangeEventStartDate,
handleChangeEventStartTime,
handleChangeEventEndDate,
handleChangeEventEndTime,
+ eventTitle,
handleSaveEvent,
handleDeleteEvent,
+ deleteOpen,
+ handleOpenDeleteConfirm,
+ handleCloseDeleteConfirm,
};
};
diff --git a/src/features/activities/upcoming_events/export_upcoming_events/useExportUpcomingEvents.tsx b/src/features/activities/upcoming_events/export_upcoming_events/useExportUpcomingEvents.tsx
index 13897869a7a..671b9f0ef38 100644
--- a/src/features/activities/upcoming_events/export_upcoming_events/useExportUpcomingEvents.tsx
+++ b/src/features/activities/upcoming_events/export_upcoming_events/useExportUpcomingEvents.tsx
@@ -2,51 +2,30 @@ import { useState } from 'react';
import { useAtomValue } from 'jotai';
import { pdf } from '@react-pdf/renderer';
import saveAs from 'file-saver';
-import { UpcomingEventType } from '@definition/upcoming_events';
import { getMessageByCode } from '@services/i18n/translation';
import { displaySnackNotification } from '@services/states/app';
import { congNameState, JWLangLocaleState } from '@states/settings';
-import { upcomingEventsActiveState } from '@states/upcoming_events';
-import { upcomingEventData } from '@services/app/upcoming_events';
+import { upcomingEventsByDataViewState } from '@states/upcoming_events';
+import {
+ groupEventsByYear,
+ upcomingEventData,
+} from '@services/app/upcoming_events';
import TemplateUpcomingEvents from '@views/activities/upcoming_events';
const useExportUpcomingEvents = () => {
const locale = useAtomValue(JWLangLocaleState);
const congName = useAtomValue(congNameState);
- const events = useAtomValue(upcomingEventsActiveState);
+ const events = useAtomValue(upcomingEventsByDataViewState);
const [isProcessing, setIsProcessing] = useState(false);
- const sortEventsByYear = (events: UpcomingEventType[]) => {
- const yearMap = new Map();
-
- for (const event of events) {
- if (event._deleted) continue;
-
- const dateStr = event.event_data?.start;
- if (!dateStr) continue;
-
- const year = new Date(dateStr).getFullYear();
-
- if (!yearMap.has(year)) {
- yearMap.set(year, []);
- }
-
- yearMap.get(year)!.push(event);
- }
-
- const sortedYears = Array.from(yearMap.keys()).sort((a, b) => a - b);
-
- return sortedYears.map((year) => yearMap.get(year)!);
- };
-
const handleExport = async () => {
if (isProcessing) return;
try {
setIsProcessing(true);
- const rawSortedEvents = sortEventsByYear(events);
+ const rawSortedEvents = groupEventsByYear(events);
const eventsForPDF = rawSortedEvents.map((rawEventsGroup) => {
return rawEventsGroup.map((eventForPDF) => {
diff --git a/src/features/activities/upcoming_events/quick_settings/index.tsx b/src/features/activities/upcoming_events/quick_settings/index.tsx
new file mode 100644
index 00000000000..1b06ff77298
--- /dev/null
+++ b/src/features/activities/upcoming_events/quick_settings/index.tsx
@@ -0,0 +1,22 @@
+import { Stack } from '@mui/material';
+import { useAppTranslation } from '@hooks/index';
+import { QuickSettingsUpcomingEventsProps } from './index.types';
+import MultiDayDisplay from '@features/congregation/settings/meeting_forms/multiday_display';
+import QuickSettings from '@features/quick_settings';
+
+const QuickSettingsUpcomingEvents = ({
+ onClose,
+ open,
+}: QuickSettingsUpcomingEventsProps) => {
+ const { t } = useAppTranslation();
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default QuickSettingsUpcomingEvents;
diff --git a/src/features/activities/upcoming_events/quick_settings/index.types.ts b/src/features/activities/upcoming_events/quick_settings/index.types.ts
new file mode 100644
index 00000000000..05923e7ddcc
--- /dev/null
+++ b/src/features/activities/upcoming_events/quick_settings/index.types.ts
@@ -0,0 +1,4 @@
+export type QuickSettingsUpcomingEventsProps = {
+ open: boolean;
+ onClose: VoidFunction;
+};
diff --git a/src/features/activities/upcoming_events/upcoming_event/index.tsx b/src/features/activities/upcoming_events/upcoming_event/index.tsx
index af48b7fae0a..546359d9a3f 100644
--- a/src/features/activities/upcoming_events/upcoming_event/index.tsx
+++ b/src/features/activities/upcoming_events/upcoming_event/index.tsx
@@ -140,15 +140,16 @@ const UpcomingEvent = (props: UpcomingEventProps) => {
{props.data.event_data.duration === UpcomingEventDuration.SingleDay && (
)}
- {props.data.event_data.category ===
- UpcomingEventCategory.SpecialCampaignWeek && (
+ {eventFormatted.showAsRange && (
{
)}
{props.data.event_data.duration === UpcomingEventDuration.MultipleDays &&
- props.data.event_data.category !==
- UpcomingEventCategory.SpecialCampaignWeek &&
+ !eventFormatted.showAsRange &&
eventFormatted.dates.map((eventDate, eventDateIndex) => (
{
};
export default UpcomingEvent;
-
diff --git a/src/features/activities/upcoming_events/upcoming_event/useUpcomingEvent.tsx b/src/features/activities/upcoming_events/upcoming_event/useUpcomingEvent.tsx
index eaad787b7c5..4f6104f7276 100644
--- a/src/features/activities/upcoming_events/upcoming_event/useUpcomingEvent.tsx
+++ b/src/features/activities/upcoming_events/upcoming_event/useUpcomingEvent.tsx
@@ -1,5 +1,10 @@
import { useEffect, useMemo, useRef, useState } from 'react';
+import { useAtomValue } from 'jotai';
import { IconError } from '@components/icons';
+import {
+ eventsMultiDayDisplayState,
+ hour24FormatState,
+} from '@states/settings';
import { dbUpcomingEventsSave } from '@services/dexie/upcoming_events';
import { UpcomingEventType } from '@definition/upcoming_events';
import { displaySnackNotification } from '@services/states/app';
@@ -10,6 +15,9 @@ import { decorationsForEvent } from '../decorations_for_event';
import { UpcomingEventProps } from './index.types';
const useUpcomingEvent = ({ data }: UpcomingEventProps) => {
+ const multiDayDisplay = useAtomValue(eventsMultiDayDisplayState);
+ const hour24 = useAtomValue(hour24FormatState);
+
const dayIndicatorRefs = useRef([]);
const [isEdit, setIsEdit] = useState(false);
@@ -23,9 +31,12 @@ const useUpcomingEvent = ({ data }: UpcomingEventProps) => {
return formatDate(result, 'yyyy/MM/dd');
}, []);
+ // upcomingEventData reads these settings straight from the store, so they
+ // have to be dependencies for the list to react when either changes
const eventFormatted = useMemo(() => {
return upcomingEventData(data);
- }, [data]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, multiDayDisplay, hour24]);
const eventDecoration = useMemo(() => {
const category = data.event_data.category;
diff --git a/src/features/activities/upcoming_events/upcoming_events_list/useUpcomingEventsList.tsx b/src/features/activities/upcoming_events/upcoming_events_list/useUpcomingEventsList.tsx
index f17f57dbaf4..9b5e0ebdae2 100644
--- a/src/features/activities/upcoming_events/upcoming_events_list/useUpcomingEventsList.tsx
+++ b/src/features/activities/upcoming_events/upcoming_events_list/useUpcomingEventsList.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { UpcomingEventsListProps } from './index.types';
-import { UpcomingEventType } from '@definition/upcoming_events';
+import { groupEventsByYear } from '@services/app/upcoming_events';
import useCurrentUser from '@hooks/useCurrentUser';
const useUpcomingEventsList = ({ data }: UpcomingEventsListProps) => {
@@ -14,27 +14,7 @@ const useUpcomingEventsList = ({ data }: UpcomingEventsListProps) => {
new Set()
);
- const eventsSortedByYear = useMemo(() => {
- const yearMap = new Map();
-
- for (const event of data) {
- const dateStr = event.event_data?.start;
-
- if (!dateStr) continue;
-
- const year = new Date(dateStr).getFullYear();
-
- if (!yearMap.has(year)) {
- yearMap.set(year, []);
- }
-
- yearMap.get(year)!.push(event);
- }
-
- const sortedYears = Array.from(yearMap.keys()).sort((a, b) => a - b);
-
- return sortedYears.map((year) => yearMap.get(year)!);
- }, [data]);
+ const eventsSortedByYear = useMemo(() => groupEventsByYear(data), [data]);
useEffect(() => {
const handleScroll = () => {
diff --git a/src/features/congregation/settings/language_groups/group_add/useGroupAdd.tsx b/src/features/congregation/settings/language_groups/group_add/useGroupAdd.tsx
index 2cd44bdf61b..6497a95902d 100644
--- a/src/features/congregation/settings/language_groups/group_add/useGroupAdd.tsx
+++ b/src/features/congregation/settings/language_groups/group_add/useGroupAdd.tsx
@@ -141,6 +141,18 @@ const useGroupAdd = ({ onClose }: GroupAddProps) => {
value: true,
});
+ const eventsDisplay = structuredClone(
+ appSettings.cong_settings.events_multiday_display ||
+ settingSchema.cong_settings.events_multiday_display
+ );
+
+ eventsDisplay.push({
+ _deleted: false,
+ type: group.group_id,
+ updatedAt: new Date().toISOString(),
+ value: 'byDay',
+ });
+
const onlineRecord = appSettings.cong_settings.attendance_online_record;
onlineRecord.push({
@@ -193,6 +205,7 @@ const useGroupAdd = ({ onClose }: GroupAddProps) => {
'cong_settings.fullname_option': fullnameOption,
'cong_settings.short_date_format': shortDateFormat,
'cong_settings.format_24h_enabled': format24h,
+ 'cong_settings.events_multiday_display': eventsDisplay,
'cong_settings.attendance_online_record': onlineRecord,
'cong_settings.first_day_week': firstDayWeek,
'cong_settings.schedule_songs_weekend': weekendSongs,
diff --git a/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx b/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx
index 28644da87ee..9f9ba5deac5 100644
--- a/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx
+++ b/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx
@@ -126,6 +126,20 @@ const useGroupDelete = ({ group }: GroupDeleteProps) => {
find24h.updatedAt = new Date().toISOString();
}
+ const eventsDisplay = structuredClone(
+ settings.cong_settings.events_multiday_display ||
+ settingSchema.cong_settings.events_multiday_display
+ );
+
+ const findEventsDisplay = eventsDisplay.find(
+ (record) => record.type === group.group_id
+ );
+
+ if (findEventsDisplay) {
+ findEventsDisplay._deleted = true;
+ findEventsDisplay.updatedAt = new Date().toISOString();
+ }
+
const onlineRecord = structuredClone(
settings.cong_settings.attendance_online_record
);
@@ -204,6 +218,7 @@ const useGroupDelete = ({ group }: GroupDeleteProps) => {
'cong_settings.fullname_option': fullnameOption,
'cong_settings.short_date_format': shortDateFormat,
'cong_settings.format_24h_enabled': format24h,
+ 'cong_settings.events_multiday_display': eventsDisplay,
'cong_settings.attendance_online_record': onlineRecord,
'cong_settings.first_day_week': firstDayWeek,
'cong_settings.schedule_songs_weekend': weekendSongs,
diff --git a/src/features/congregation/settings/meeting_forms/index.tsx b/src/features/congregation/settings/meeting_forms/index.tsx
index e04901e352c..dd01c43dc29 100644
--- a/src/features/congregation/settings/meeting_forms/index.tsx
+++ b/src/features/congregation/settings/meeting_forms/index.tsx
@@ -12,6 +12,7 @@ import DateFormat from './date_format';
import DisplayName from './display_name';
import MenuItem from '@components/menuitem';
import MidweekExactDate from './midweek_exact_date';
+import MultiDayDisplay from './multiday_display';
import NameFormat from './name_format';
import PublishersSort from '../congregation_privacy/publishers_sort';
import Select from '@components/select';
@@ -102,6 +103,11 @@ const MeetingForms = () => {
)}
+
+
+
+
+
);
diff --git a/src/features/congregation/settings/meeting_forms/multiday_display/index.tsx b/src/features/congregation/settings/meeting_forms/multiday_display/index.tsx
new file mode 100644
index 00000000000..1cfb95a99ce
--- /dev/null
+++ b/src/features/congregation/settings/meeting_forms/multiday_display/index.tsx
@@ -0,0 +1,40 @@
+import { Box } from '@mui/material';
+import { useAppTranslation, useCurrentUser } from '@hooks/index';
+import {
+ UPCOMING_EVENT_MAX_LIST_DAYS,
+ UpcomingEventDisplayType,
+} from '@definition/upcoming_events';
+import useMultiDayDisplay from './useMultiDayDisplay';
+import MenuItem from '@components/menuitem';
+import Select from '@components/select';
+import Typography from '@components/typography';
+
+const MultiDayDisplay = () => {
+ const { t } = useAppTranslation();
+
+ const { isAdmin } = useCurrentUser();
+
+ const { display, handleDisplayChange } = useMultiDayDisplay();
+
+ return (
+
+
+
+
+ {t('tr_multiDayEventsHint', { days: UPCOMING_EVENT_MAX_LIST_DAYS })}
+
+
+ );
+};
+
+export default MultiDayDisplay;
diff --git a/src/features/congregation/settings/meeting_forms/multiday_display/useMultiDayDisplay.tsx b/src/features/congregation/settings/meeting_forms/multiday_display/useMultiDayDisplay.tsx
new file mode 100644
index 00000000000..bdff63282bf
--- /dev/null
+++ b/src/features/congregation/settings/meeting_forms/multiday_display/useMultiDayDisplay.tsx
@@ -0,0 +1,40 @@
+import { useAtomValue } from 'jotai';
+import { UpcomingEventDisplayType } from '@definition/upcoming_events';
+import { dbAppSettingsUpdate } from '@services/dexie/settings';
+import { settingSchema } from '@services/dexie/schema';
+import {
+ eventsMultiDayDisplayState,
+ settingsState,
+ userDataViewState,
+} from '@states/settings';
+
+const useMultiDayDisplay = () => {
+ const settings = useAtomValue(settingsState);
+ const dataView = useAtomValue(userDataViewState);
+ const display = useAtomValue(eventsMultiDayDisplayState);
+
+ const handleDisplayChange = async (value: UpcomingEventDisplayType) => {
+ const records = structuredClone(
+ settings.cong_settings.events_multiday_display ||
+ settingSchema.cong_settings.events_multiday_display
+ );
+
+ let current = records.find((record) => record.type === dataView);
+
+ if (!current) {
+ current = { type: dataView, value, updatedAt: '', _deleted: false };
+ records.push(current);
+ }
+
+ current.value = value;
+ current.updatedAt = new Date().toISOString();
+
+ await dbAppSettingsUpdate({
+ 'cong_settings.events_multiday_display': records,
+ });
+ };
+
+ return { display, handleDisplayChange };
+};
+
+export default useMultiDayDisplay;
diff --git a/src/locales/en/congregation.json b/src/locales/en/congregation.json
index 0913da93a82..8632035e0e4 100644
--- a/src/locales/en/congregation.json
+++ b/src/locales/en/congregation.json
@@ -511,6 +511,12 @@
"tr_singleDay": "Single day",
"tr_multipleDays": "Multiple days",
"tr_wholeDay": "Whole day",
+ "tr_multiDayEventsLayout": "Multi-day events layout",
+ "tr_separateDays": "Separate days",
+ "tr_oneDateRange": "One date range",
+ "tr_deleteEventTitle": "Delete the event",
+ "tr_deleteEventDesc": "Are you sure you want to delete “{{ eventName }}”? This cannot be undone.",
+ "tr_multiDayEventsHint": "Events longer than {{ days }} days always use a date range",
"tr_groupServant": "Group servant",
"tr_groupServantAssign": "Make servant",
"tr_firstDayOfTheWeek": "First day of the week",
diff --git a/src/locales/en/general.json b/src/locales/en/general.json
index 34470d3a912..40af2c814ba 100644
--- a/src/locales/en/general.json
+++ b/src/locales/en/general.json
@@ -149,6 +149,8 @@
"tr_created": "Created: {{ date }}",
"tr_missingInfo": "Missing info",
"tr_fillRequiredField": "Please fill in the required field",
+ "tr_endTimeAfterStart": "End time must be after the start time",
+ "tr_endDateAfterStart": "End date must be after the start date",
"tr_mondayShort": "Mon",
"tr_tuesdayShort": "Tue",
"tr_wednesdayShort": "Wed",
diff --git a/src/pages/activities/upcoming_events/index.tsx b/src/pages/activities/upcoming_events/index.tsx
index bd7f4240f2c..0f6337741d1 100644
--- a/src/pages/activities/upcoming_events/index.tsx
+++ b/src/pages/activities/upcoming_events/index.tsx
@@ -4,6 +4,7 @@ import { useAppTranslation, useBreakpoints } from '@hooks/index';
import useUpcomingEvents from './useUpcomingEvents';
import EditUpcomingEvent from '@features/activities/upcoming_events/edit_upcoming_event';
import PageTitle from '@components/page_title';
+import QuickSettingsUpcomingEvents from '@features/activities/upcoming_events/quick_settings';
import UpcomingEventsList from '@features/activities/upcoming_events/upcoming_events_list';
import ExportUpcomingEvents from '@features/activities/upcoming_events/export_upcoming_events';
import NavBarButton from '@components/nav_bar_button';
@@ -21,6 +22,9 @@ const UpcomingEvents = () => {
handleHideAddEventBox,
addEventBoxShow,
handleSaveEvent,
+ quickSettingsOpen,
+ handleOpenQuickSettings,
+ handleCloseQuickSettings,
} = useUpcomingEvents();
return (
@@ -32,8 +36,16 @@ const UpcomingEvents = () => {
paddingBottom: !tablet688Up ? '60px' : '0px',
}}
>
+ {quickSettingsOpen && (
+
+ )}
+
diff --git a/src/pages/activities/upcoming_events/useUpcomingEvents.tsx b/src/pages/activities/upcoming_events/useUpcomingEvents.tsx
index 4e273fb5ab7..9e93c213735 100644
--- a/src/pages/activities/upcoming_events/useUpcomingEvents.tsx
+++ b/src/pages/activities/upcoming_events/useUpcomingEvents.tsx
@@ -1,10 +1,13 @@
-import { useMemo, useState } from 'react';
+import { useState } from 'react';
import { useAtomValue } from 'jotai';
import { IconError } from '@components/icons';
import { useCurrentUser } from '@hooks/index';
-import { UpcomingEventType } from '@definition/upcoming_events';
+import {
+ DEFAULT_EVENT_START_HOUR,
+ UpcomingEventType,
+} from '@definition/upcoming_events';
import { dbUpcomingEventsSave } from '@services/dexie/upcoming_events';
-import { upcomingEventsActiveState } from '@states/upcoming_events';
+import { upcomingEventsByDataViewState } from '@states/upcoming_events';
import { addHours } from '@utils/date';
import { displaySnackNotification } from '@services/states/app';
import { getMessageByCode } from '@services/i18n/translation';
@@ -13,31 +16,24 @@ import { userDataViewState } from '@states/settings';
const useUpcomingEvents = () => {
const { isAdmin } = useCurrentUser();
- const upcomingEvents = useAtomValue(upcomingEventsActiveState);
+ const events = useAtomValue(upcomingEventsByDataViewState);
const dataView = useAtomValue(userDataViewState);
const [addEventBoxShow, setAddEventBoxShow] = useState(false);
+ const [quickSettingsOpen, setQuickSettingsOpen] = useState(false);
- const events = useMemo(() => {
- return upcomingEvents.filter((record) => {
- if (dataView === 'main') {
- return record.event_data.type === 'main';
- }
-
- // language group events (main + own events)
- return (
- record.event_data.type === 'main' || record.event_data.type === dataView
- );
- });
- }, [upcomingEvents, dataView]);
+ const start = new Date();
+ start.setHours(DEFAULT_EVENT_START_HOUR, 0, 0, 0);
+ // deliberately not memoised: the add form seeds its state from this on
+ // mount, so a stable uid would make a second event save over the first
const emptyEvent: UpcomingEventType = {
event_uid: crypto.randomUUID(),
event_data: {
_deleted: false,
updatedAt: new Date().toISOString(),
- start: new Date().toISOString(),
- end: addHours(5).toISOString(),
+ start: start.toISOString(),
+ end: addHours(1, start).toISOString(),
description: '',
type: dataView,
custom: '',
@@ -58,6 +54,10 @@ const useUpcomingEvents = () => {
handleShowAddEventBox();
};
+ const handleOpenQuickSettings = () => setQuickSettingsOpen(true);
+
+ const handleCloseQuickSettings = () => setQuickSettingsOpen(false);
+
const handleSaveEvent = async (event: UpcomingEventType) => {
try {
await dbUpcomingEventsSave(event);
@@ -82,6 +82,9 @@ const useUpcomingEvents = () => {
handleSaveEvent,
handleHideAddEventBox,
handleAddEventButtonClick,
+ quickSettingsOpen,
+ handleOpenQuickSettings,
+ handleCloseQuickSettings,
};
};
diff --git a/src/services/app/upcoming_events.ts b/src/services/app/upcoming_events.ts
index 4e7d5351bff..e5de0cc60d6 100644
--- a/src/services/app/upcoming_events.ts
+++ b/src/services/app/upcoming_events.ts
@@ -1,6 +1,7 @@
import {
- UpcomingEventCategory,
+ UPCOMING_EVENT_MAX_LIST_DAYS,
UpcomingEventDataType,
+ UpcomingEventDuration,
UpcomingEventType,
} from '@definition/upcoming_events';
import { formatDate, getDatesBetweenDates } from '@utils/date';
@@ -10,7 +11,39 @@ import {
getTranslation,
} from '@services/i18n/translation';
import { store } from '@states/index';
-import { hour24FormatState } from '@states/settings';
+import {
+ eventsMultiDayDisplayState,
+ hour24FormatState,
+} from '@states/settings';
+
+export const groupEventsByYear = (events: UpcomingEventType[]) => {
+ const yearMap = new Map();
+
+ for (const event of events) {
+ const start = event.event_data?.start;
+
+ if (!start) continue;
+
+ const year = new Date(start).getFullYear();
+
+ if (!yearMap.has(year)) {
+ yearMap.set(year, []);
+ }
+
+ yearMap.get(year)!.push(event);
+ }
+
+ return Array.from(yearMap.keys())
+ .sort((a, b) => a - b)
+ .map((year) => yearMap.get(year)!);
+};
+
+export const isWholeDayEvent = (event: UpcomingEventType) => {
+ return (
+ event.event_data.wholeDay ??
+ event.event_data.duration === UpcomingEventDuration.MultipleDays
+ );
+};
export const upcomingEventData = (event: UpcomingEventType) => {
const hour24 = store.get(hour24FormatState);
@@ -25,6 +58,7 @@ export const upcomingEventData = (event: UpcomingEventType) => {
result.custom = event.event_data.custom;
result.description = event.event_data.description;
result.duration = event.event_data.duration;
+ result.wholeDay = isWholeDayEvent(event);
result.year = new Date(event.event_data.start).getFullYear();
@@ -64,7 +98,13 @@ export const upcomingEventData = (event: UpcomingEventType) => {
};
});
- if (event.event_data.category === UpcomingEventCategory.SpecialCampaignWeek) {
+ result.showAsRange =
+ event.event_data.duration === UpcomingEventDuration.MultipleDays &&
+ eventDates.length > 1 &&
+ (eventDates.length > UPCOMING_EVENT_MAX_LIST_DAYS ||
+ store.get(eventsMultiDayDisplayState) === 'range');
+
+ if (eventDates.length > 1) {
const startDate = eventDates.at(0);
const startDateV = startDate.getDate();
const startMonthIndex = startDate.getMonth();
diff --git a/src/services/dexie/schema.ts b/src/services/dexie/schema.ts
index c39bfd275b2..3999be85bb6 100644
--- a/src/services/dexie/schema.ts
+++ b/src/services/dexie/schema.ts
@@ -277,6 +277,9 @@ export const settingSchema: SettingsType = {
format_24h_enabled: [
{ type: 'main', value: true, updatedAt: '', _deleted: false },
],
+ events_multiday_display: [
+ { type: 'main', value: 'byDay', updatedAt: '', _deleted: false },
+ ],
week_start_sunday: [
{ type: 'main', value: false, updatedAt: '', _deleted: false },
],
diff --git a/src/states/settings.ts b/src/states/settings.ts
index 540fa4d6dd4..8a2c5af52de 100644
--- a/src/states/settings.ts
+++ b/src/states/settings.ts
@@ -116,6 +116,17 @@ export const hour24FormatState = atom((get) => {
);
});
+export const eventsMultiDayDisplayState = atom((get) => {
+ const settings = get(settingsState);
+ const dataView = get(userDataViewState);
+
+ return (
+ settings.cong_settings.events_multiday_display?.find(
+ (record) => record.type === dataView
+ )?.value ?? 'byDay'
+ );
+});
+
export const COFirstnameState = atom((get) => {
const settings = get(settingsState);
diff --git a/src/states/upcoming_events.ts b/src/states/upcoming_events.ts
index f7c61b27e49..3549b61a918 100644
--- a/src/states/upcoming_events.ts
+++ b/src/states/upcoming_events.ts
@@ -5,6 +5,7 @@ This file holds the source of the truth from the table "upcoming_events".
import { atom } from 'jotai';
import { UpcomingEventType } from '@definition/upcoming_events';
import { formatDate } from '@utils/date';
+import { userDataViewState } from './settings';
export const upcomingEventsDbState = atom([]);
@@ -32,3 +33,18 @@ export const upcomingEventsActiveState = atom((get) => {
return startDate >= today || endDate >= today;
});
});
+
+export const upcomingEventsByDataViewState = atom((get) => {
+ const events = get(upcomingEventsActiveState);
+ const dataView = get(userDataViewState);
+
+ return events.filter((record) => {
+ if (dataView === 'main') {
+ return record.event_data.type === 'main';
+ }
+
+ return (
+ record.event_data.type === 'main' || record.event_data.type === dataView
+ );
+ });
+});
diff --git a/src/utils/date.ts b/src/utils/date.ts
index f8eed596e14..798a0c94d77 100644
--- a/src/utils/date.ts
+++ b/src/utils/date.ts
@@ -510,6 +510,15 @@ export const addHours = (amount: number, date: Date = new Date()) => {
return libAddHours(date, amount);
};
+export const sundayOfWeek = (date: Date) => {
+ const result = new Date(date);
+ const weekday = result.getDay();
+
+ result.setDate(result.getDate() + (weekday === 0 ? 0 : 7 - weekday));
+
+ return result;
+};
+
export const formatDateShortMonthWithYear = (
value: string,
language?: string
diff --git a/src/views/activities/upcoming_events/UpcomingEvent.tsx b/src/views/activities/upcoming_events/UpcomingEvent.tsx
index c9e246990b1..0821c424708 100644
--- a/src/views/activities/upcoming_events/UpcomingEvent.tsx
+++ b/src/views/activities/upcoming_events/UpcomingEvent.tsx
@@ -12,6 +12,9 @@ import UpcomingEventDate from './UpcomingEventDate';
const UpcomingEvent = ({ event }: UpcomingEventProps) => {
const { t } = useAppTranslation();
+ const decoration =
+ decorationsForEvent[event.category] ?? decorationsForEvent.at(-1);
+
return (
{
gap: '2px',
}}
>
- {cloneElement(decorationsForEvent[event.category].icon, {
+ {cloneElement(decoration.icon, {
size: 14,
backgroundColor: 'none',
})}
@@ -41,7 +44,7 @@ const UpcomingEvent = ({ event }: UpcomingEventProps) => {
style={{ fontWeight: 500, fontSize: '11px', color: '#222222' }}
>
{event.category !== UpcomingEventCategory.Custom
- ? t(decorationsForEvent[event.category].translationKey)
+ ? t(decoration.translationKey)
: event.custom}
@@ -55,23 +58,26 @@ const UpcomingEvent = ({ event }: UpcomingEventProps) => {
)}
{event.duration === UpcomingEventDuration.MultipleDays &&
- event.category !== UpcomingEventCategory.SpecialCampaignWeek &&
- event.dates.map((eventDate, eventDateIndex) => (
-
- ))}
+ !event.showAsRange && (
+ <>
+ {event.dates.map((eventDate, eventDateIndex) => (
+
+ ))}
+ >
+ )}
- {event.category === UpcomingEventCategory.SpecialCampaignWeek && (
+ {event.showAsRange && (