feat: add next event indicator to Show Page Calendar tab (#4348)
* feat: add next event indicator to Show Page Calendar tab Closes #4289 * feat: improve calendar animation * refactor: add some utils and fix sorting edge case with full day * refactor: rename CalendarCurrentEventIndicator to CalendarCurrentEventCursor * fix: fix tests * Fix lint --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -0,0 +1,80 @@
|
||||
import { addDays, addHours, startOfDay, subDays, subHours } from 'date-fns';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
|
||||
import { findUpcomingCalendarEvent } from '../findUpcomingCalendarEvent';
|
||||
|
||||
const pastEvent: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'> = {
|
||||
startsAt: subHours(new Date(), 2),
|
||||
endsAt: subHours(new Date(), 1),
|
||||
isFullDay: false,
|
||||
};
|
||||
const fullDayPastEvent: Pick<CalendarEvent, 'startsAt' | 'isFullDay'> = {
|
||||
startsAt: subDays(new Date(), 1),
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
const currentEvent: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'> = {
|
||||
startsAt: addHours(new Date(), 1),
|
||||
endsAt: addHours(new Date(), 2),
|
||||
isFullDay: false,
|
||||
};
|
||||
const currentFullDayEvent: Pick<CalendarEvent, 'startsAt' | 'isFullDay'> = {
|
||||
startsAt: startOfDay(new Date()),
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
const futureEvent: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'> = {
|
||||
startsAt: addDays(new Date(), 1),
|
||||
endsAt: addDays(new Date(), 2),
|
||||
isFullDay: false,
|
||||
};
|
||||
const fullDayFutureEvent: Pick<CalendarEvent, 'startsAt' | 'isFullDay'> = {
|
||||
startsAt: addDays(new Date(), 2),
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
describe('findUpcomingCalendarEvent', () => {
|
||||
it('returns the first current event by chronological order', () => {
|
||||
// Given
|
||||
const calendarEvents = [
|
||||
futureEvent,
|
||||
currentFullDayEvent,
|
||||
pastEvent,
|
||||
currentEvent,
|
||||
];
|
||||
|
||||
// When
|
||||
const result = findUpcomingCalendarEvent(calendarEvents);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(currentFullDayEvent);
|
||||
});
|
||||
|
||||
it('returns the next future event by chronological order', () => {
|
||||
// Given
|
||||
const calendarEvents = [
|
||||
fullDayPastEvent,
|
||||
fullDayFutureEvent,
|
||||
futureEvent,
|
||||
pastEvent,
|
||||
];
|
||||
|
||||
// When
|
||||
const result = findUpcomingCalendarEvent(calendarEvents);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(futureEvent);
|
||||
});
|
||||
|
||||
it('returns undefined if all events are in the past', () => {
|
||||
// Given
|
||||
const calendarEvents = [pastEvent, fullDayPastEvent];
|
||||
|
||||
// When
|
||||
const result = findUpcomingCalendarEvent(calendarEvents);
|
||||
|
||||
// Then
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,105 @@
|
||||
import { addDays, addHours, subDays, subHours } from 'date-fns';
|
||||
|
||||
import { hasCalendarEventEnded } from '../hasCalendarEventEnded';
|
||||
|
||||
describe('hasCalendarEventEnded', () => {
|
||||
describe('Event with end date', () => {
|
||||
it('returns true for an event with a past end date', () => {
|
||||
// Given
|
||||
const startsAt = subHours(new Date(), 2);
|
||||
const endsAt = subHours(new Date(), 1);
|
||||
const isFullDay = false;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
endsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an event if end date is now', () => {
|
||||
// Given
|
||||
const startsAt = subHours(new Date(), 1);
|
||||
const endsAt = new Date();
|
||||
const isFullDay = false;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
endsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an event with a future end date', () => {
|
||||
// Given
|
||||
const startsAt = new Date();
|
||||
const endsAt = addHours(new Date(), 1);
|
||||
const isFullDay = false;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
endsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full day event', () => {
|
||||
it('returns true for a past full day event', () => {
|
||||
// Given
|
||||
const startsAt = subDays(new Date(), 1);
|
||||
const isFullDay = true;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a future full day event', () => {
|
||||
// Given
|
||||
const startsAt = addDays(new Date(), 1);
|
||||
const isFullDay = true;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if the full day event is today', () => {
|
||||
// Given
|
||||
const startsAt = new Date();
|
||||
const isFullDay = true;
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventEnded({
|
||||
startsAt,
|
||||
isFullDay,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import { addHours, subHours } from 'date-fns';
|
||||
|
||||
import { hasCalendarEventStarted } from '../hasCalendarEventStarted';
|
||||
|
||||
describe('hasCalendarEventStarted', () => {
|
||||
it('returns true for an event with a past start date', () => {
|
||||
// Given
|
||||
const startsAt = subHours(new Date(), 2);
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventStarted({ startsAt });
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an event if start date is now', () => {
|
||||
// Given
|
||||
const startsAt = new Date();
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventStarted({ startsAt });
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an event with a future start date', () => {
|
||||
// Given
|
||||
const startsAt = addHours(new Date(), 1);
|
||||
|
||||
// When
|
||||
const result = hasCalendarEventStarted({ startsAt });
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -16,10 +16,12 @@ describe('sortCalendarEventsAsc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusOneHour,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusTwoHours,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -36,10 +38,12 @@ describe('sortCalendarEventsAsc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusTwoHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -56,10 +60,12 @@ describe('sortCalendarEventsAsc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusTwoHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -76,10 +82,12 @@ describe('sortCalendarEventsAsc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -95,9 +103,11 @@ describe('sortCalendarEventsAsc', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -109,13 +119,15 @@ describe('sortCalendarEventsAsc', () => {
|
||||
expect(invertedArgsResult).toBe(1);
|
||||
});
|
||||
|
||||
it('returns 0 for events with same start date and no end date', () => {
|
||||
it('returns 0 for full day events with the same start date', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -127,14 +139,16 @@ describe('sortCalendarEventsAsc', () => {
|
||||
expect(invertedArgsResult).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 for events with same start date if one of them has no end date', () => {
|
||||
it('sorts the full day event last for two events with the same start date if one is full day', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusOneHour,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -142,8 +156,8 @@ describe('sortCalendarEventsAsc', () => {
|
||||
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(0);
|
||||
expect(invertedArgsResult).toBe(0);
|
||||
expect(result).toBe(-1);
|
||||
expect(invertedArgsResult).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -153,10 +167,12 @@ describe('sortCalendarEventsDesc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusOneHour,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusTwoHours,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -173,10 +189,12 @@ describe('sortCalendarEventsDesc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusTwoHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -193,10 +211,12 @@ describe('sortCalendarEventsDesc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusTwoHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -213,10 +233,12 @@ describe('sortCalendarEventsDesc', () => {
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
endsAt: someDatePlusThreeHours,
|
||||
isFullDay: false,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -232,9 +254,11 @@ describe('sortCalendarEventsDesc', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDatePlusOneHour,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -246,13 +270,15 @@ describe('sortCalendarEventsDesc', () => {
|
||||
expect(invertedArgsResult).toBe(-1);
|
||||
});
|
||||
|
||||
it('returns 0 for events with same start date and no end date', () => {
|
||||
it('returns 0 for full day events with the same start date', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -264,14 +290,16 @@ describe('sortCalendarEventsDesc', () => {
|
||||
expect(invertedArgsResult === 0).toBe(true);
|
||||
});
|
||||
|
||||
it('returns 0 for events with same start date if one of them has no end date', () => {
|
||||
it('sorts the full day event first for two events with the same start date if one is full day', () => {
|
||||
// Given
|
||||
const eventA = {
|
||||
startsAt: someDate,
|
||||
endsAt: someDatePlusOneHour,
|
||||
isFullDay: false,
|
||||
};
|
||||
const eventB = {
|
||||
startsAt: someDate,
|
||||
isFullDay: true,
|
||||
};
|
||||
|
||||
// When
|
||||
@ -279,7 +307,7 @@ describe('sortCalendarEventsDesc', () => {
|
||||
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||
|
||||
// Then
|
||||
expect(result === 0).toBe(true);
|
||||
expect(invertedArgsResult === 0).toBe(true);
|
||||
expect(result).toBe(1);
|
||||
expect(invertedArgsResult).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
|
||||
import { sortCalendarEventsAsc } from '@/activities/calendar/utils/sortCalendarEvents';
|
||||
|
||||
export const findUpcomingCalendarEvent = <
|
||||
T extends Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'>,
|
||||
>(
|
||||
calendarEvents: T[],
|
||||
) =>
|
||||
[...calendarEvents]
|
||||
.sort(sortCalendarEventsAsc)
|
||||
.find((calendarEvent) => !hasCalendarEventEnded(calendarEvent));
|
||||
@ -0,0 +1,7 @@
|
||||
import { endOfDay } from 'date-fns';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
|
||||
export const getCalendarEventEndDate = (
|
||||
calendarEvent: Pick<CalendarEvent, 'endsAt' | 'isFullDay' | 'startsAt'>,
|
||||
) => calendarEvent.endsAt ?? endOfDay(calendarEvent.startsAt);
|
||||
@ -0,0 +1,8 @@
|
||||
import { isPast } from 'date-fns';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
|
||||
|
||||
export const hasCalendarEventEnded = (
|
||||
calendarEvent: Pick<CalendarEvent, 'endsAt' | 'isFullDay' | 'startsAt'>,
|
||||
) => isPast(getCalendarEventEndDate(calendarEvent));
|
||||
@ -0,0 +1,7 @@
|
||||
import { isPast } from 'date-fns';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
|
||||
export const hasCalendarEventStarted = (
|
||||
calendarEvent: Pick<CalendarEvent, 'startsAt'>,
|
||||
) => isPast(calendarEvent.startsAt);
|
||||
@ -1,31 +1,27 @@
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
|
||||
import { sortAsc } from '~/utils/sort';
|
||||
|
||||
export const sortCalendarEventsAsc = (
|
||||
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'>,
|
||||
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'>,
|
||||
) => {
|
||||
const startsAtSort = sortAsc(
|
||||
calendarEventA.startsAt.getTime(),
|
||||
calendarEventB.startsAt.getTime(),
|
||||
);
|
||||
|
||||
if (
|
||||
startsAtSort === 0 &&
|
||||
isDefined(calendarEventA.endsAt) &&
|
||||
isDefined(calendarEventB.endsAt)
|
||||
) {
|
||||
return sortAsc(
|
||||
calendarEventA.endsAt.getTime(),
|
||||
calendarEventB.endsAt.getTime(),
|
||||
);
|
||||
if (startsAtSort === 0) {
|
||||
const endsAtA = getCalendarEventEndDate(calendarEventA);
|
||||
const endsAtB = getCalendarEventEndDate(calendarEventB);
|
||||
|
||||
return sortAsc(endsAtA.getTime(), endsAtB.getTime());
|
||||
}
|
||||
|
||||
return startsAtSort;
|
||||
};
|
||||
|
||||
export const sortCalendarEventsDesc = (
|
||||
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'>,
|
||||
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt' | 'isFullDay'>,
|
||||
) => -sortCalendarEventsAsc(calendarEventA, calendarEventB);
|
||||
|
||||
Reference in New Issue
Block a user