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 ( + + + {t('tr_deleteEventTitle')} + + + {t('tr_deleteEventDesc', { eventName: title })} + + + + + + + + + ); +}; + +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' }, }} > + handleDisplayChange(e.target.value as UpcomingEventDisplayType) + } + readOnly={!isAdmin} + > + {t('tr_separateDays')} + {t('tr_oneDateRange')} + + + + {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 && (