diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
index 97ad87b4e..a02ed3b73 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
@@ -1,14 +1,13 @@
import styled from '@emotion/styled';
-import { format, getYear, startOfMonth } from 'date-fns';
-import mapValues from 'lodash.mapvalues';
+import { format, getYear } from 'date-fns';
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
+import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
+import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents';
import { H3Title } from '@/ui/display/typography/components/H3Title';
import { Section } from '@/ui/layout/section/components/Section';
import { mockedCalendarEvents } from '~/testing/mock-data/calendar';
-import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
-import { sortDesc } from '~/utils/sort';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -27,28 +26,35 @@ export const Calendar = () => {
const sortedCalendarEvents = [...mockedCalendarEvents].sort(
sortCalendarEventsDesc,
);
- const calendarEventsByMonthTime = groupArrayItemsBy(
- sortedCalendarEvents,
- ({ startsAt }) => startOfMonth(startsAt).getTime(),
- );
- const sortedMonthTimes = Object.keys(calendarEventsByMonthTime)
- .map(Number)
- .sort(sortDesc);
- const monthTimesByYear = groupArrayItemsBy(sortedMonthTimes, getYear);
- const lastMonthTimeByYear = mapValues(monthTimesByYear, (monthTimes = []) =>
- Math.max(...monthTimes),
- );
+
+ const {
+ calendarEventsByDayTime,
+ currentCalendarEvent,
+ daysByMonthTime,
+ getNextCalendarEvent,
+ monthTimes,
+ monthTimesByYear,
+ updateCurrentCalendarEvent,
+ } = useCalendarEvents(sortedCalendarEvents);
return (
-
- {sortedMonthTimes.map((monthTime) => {
- const monthCalendarEvents = calendarEventsByMonthTime[monthTime];
- const year = getYear(monthTime);
- const isLastMonthOfYear = lastMonthTimeByYear[year] === monthTime;
- const monthLabel = format(monthTime, 'MMMM');
+
+
+ {monthTimes.map((monthTime) => {
+ const monthDayTimes = daysByMonthTime[monthTime] || [];
+ const year = getYear(monthTime);
+ const lastMonthTimeOfYear = monthTimesByYear[year]?.[0];
+ const isLastMonthOfYear = lastMonthTimeOfYear === monthTime;
+ const monthLabel = format(monthTime, 'MMMM');
- return (
- !!monthCalendarEvents?.length && (
+ return (
- )
- );
- })}
-
+ );
+ })}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx
new file mode 100644
index 000000000..8280ccbef
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx
@@ -0,0 +1,168 @@
+import { useContext, useMemo, useState } from 'react';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+ differenceInSeconds,
+ isThisMonth,
+ startOfDay,
+ startOfMonth,
+} from 'date-fns';
+import { AnimatePresence, motion } from 'framer-motion';
+
+import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
+import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
+import { hasCalendarEventStarted } from '@/activities/calendar/utils/hasCalendarEventStarted';
+
+type CalendarCurrentEventCursorProps = {
+ calendarEvent: CalendarEvent;
+};
+
+const StyledCurrentEventCursor = styled(motion.div)`
+ align-items: center;
+ background-color: ${({ theme }) => theme.font.color.danger};
+ display: inline-flex;
+ height: 1.5px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ transform: translateY(-50%);
+
+ &::before {
+ background-color: ${({ theme }) => theme.font.color.danger};
+ border-radius: 1px;
+ content: '';
+ display: block;
+ height: ${({ theme }) => theme.spacing(1)};
+ width: ${({ theme }) => theme.spacing(1)};
+ }
+`;
+
+export const CalendarCurrentEventCursor = ({
+ calendarEvent,
+}: CalendarCurrentEventCursorProps) => {
+ const theme = useTheme();
+ const {
+ calendarEventsByDayTime,
+ currentCalendarEvent,
+ getNextCalendarEvent,
+ updateCurrentCalendarEvent,
+ } = useContext(CalendarContext);
+
+ const nextCalendarEvent = getNextCalendarEvent(calendarEvent);
+ const isNextEventThisMonth =
+ !!nextCalendarEvent && isThisMonth(nextCalendarEvent.startsAt);
+
+ const calendarEventEndsAt = getCalendarEventEndDate(calendarEvent);
+
+ const isCurrent = currentCalendarEvent.id === calendarEvent.id;
+ const [hasStarted, setHasStarted] = useState(
+ hasCalendarEventStarted(calendarEvent),
+ );
+ const [hasEnded, setHasEnded] = useState(
+ hasCalendarEventEnded(calendarEvent),
+ );
+ const [isWaiting, setIsWaiting] = useState(hasEnded && !isNextEventThisMonth);
+
+ const dayTime = startOfDay(calendarEvent.startsAt).getTime();
+ const dayEvents = calendarEventsByDayTime[dayTime];
+ const isFirstEventOfDay = dayEvents?.slice(-1)[0] === calendarEvent;
+ const isLastEventOfDay = dayEvents?.[0] === calendarEvent;
+
+ const topOffset = isLastEventOfDay ? 9 : 6;
+ const bottomOffset = isFirstEventOfDay ? 9 : 6;
+
+ const currentEventCursorVariants = {
+ beforeEvent: { top: `calc(100% + ${bottomOffset}px)` },
+ eventStart: {
+ top: 'calc(100% + 3px)',
+ transition: {
+ delay: Math.max(
+ 0,
+ differenceInSeconds(calendarEvent.startsAt, new Date()),
+ ),
+ },
+ },
+ eventEnd: {
+ top: `-${topOffset}px`,
+ transition: {
+ delay: Math.max(
+ 0,
+ differenceInSeconds(calendarEventEndsAt, new Date()) + 1,
+ ),
+ },
+ },
+ fadeAway: {
+ opacity: 0,
+ top: `-${topOffset}px`,
+ transition: {
+ delay:
+ isWaiting && nextCalendarEvent
+ ? differenceInSeconds(
+ startOfMonth(nextCalendarEvent.startsAt),
+ new Date(),
+ )
+ : 0,
+ },
+ },
+ };
+
+ const animationSequence = useMemo(() => {
+ if (!hasStarted) return { initial: 'beforeEvent', animate: 'eventStart' };
+
+ if (!hasEnded) {
+ return { initial: 'eventStart', animate: 'eventEnd' };
+ }
+
+ if (!isWaiting) {
+ return { initial: undefined, animate: 'eventEnd' };
+ }
+
+ return { initial: 'eventEnd', animate: 'fadeAway' };
+ }, [hasEnded, hasStarted, isWaiting]);
+
+ return (
+
+ {isCurrent && (
+ {
+ if (stage === 'eventStart') {
+ setHasStarted(true);
+ }
+
+ if (stage === 'eventEnd') {
+ setHasEnded(true);
+
+ if (isNextEventThisMonth) {
+ updateCurrentCalendarEvent();
+ }
+ // If the next event is not the same month as the current event,
+ // we don't want the cursor to jump to the next month until the next month starts.
+ // => Wait for the upcoming event's month to start before moving the cursor.
+ // Example: we're in March. The previous event is February 15th, and the next event is April 10th.
+ // We want the cursor to stay in February until April starts.
+ else {
+ setIsWaiting(true);
+ }
+ }
+
+ if (isWaiting && stage === 'fadeAway') {
+ setIsWaiting(false);
+ updateCurrentCalendarEvent();
+ }
+ }}
+ transition={{
+ duration: theme.animation.duration.normal,
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx
index 8dd80ef74..cec2b37e0 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx
@@ -1,6 +1,6 @@
-import { css } from '@emotion/react';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { endOfDay, format, isPast } from 'date-fns';
+import { differenceInSeconds, endOfDay, format } from 'date-fns';
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
@@ -11,19 +11,13 @@ type CalendarDayCardContentProps = {
divider?: boolean;
};
-const StyledCardContent = styled(CardContent)<{ active: boolean }>`
+const StyledCardContent = styled(CardContent)`
align-items: flex-start;
border-color: ${({ theme }) => theme.border.color.light};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(3)};
padding: ${({ theme }) => theme.spacing(2, 3)};
-
- ${({ active }) =>
- !active &&
- css`
- background-color: transparent;
- `}
`;
const StyledDayContainer = styled.div`
@@ -37,7 +31,7 @@ const StyledWeekDay = styled.div`
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
-const StyledDateDay = styled.div`
+const StyledMonthDay = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
@@ -57,14 +51,33 @@ export const CalendarDayCardContent = ({
calendarEvents,
divider,
}: CalendarDayCardContentProps) => {
+ const theme = useTheme();
+
const endOfDayDate = endOfDay(calendarEvents[0].startsAt);
- const isPastDay = isPast(endOfDayDate);
+ const endsIn = differenceInSeconds(endOfDayDate, Date.now());
+
+ const weekDayLabel = format(endOfDayDate, 'EE');
+ const monthDayLabel = format(endOfDayDate, 'dd');
+
+ const upcomingDayCardContentVariants = {
+ upcoming: {},
+ ended: { backgroundColor: theme.background.primary },
+ };
return (
-
+
- {format(endOfDayDate, 'EE')}
- {format(endOfDayDate, 'dd')}
+ {weekDayLabel}
+ {monthDayLabel}
{calendarEvents.map((calendarEvent) => (
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
index 1e61d9be6..8cd98d60e 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
@@ -1,8 +1,11 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { endOfDay, format, isPast } from 'date-fns';
+import { format } from 'date-fns';
+import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
+import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { IconArrowRight, IconLock } from '@/ui/display/icon';
import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent';
@@ -17,12 +20,14 @@ const StyledContainer = styled.div`
display: inline-flex;
gap: ${({ theme }) => theme.spacing(3)};
height: ${({ theme }) => theme.spacing(6)};
+ position: relative;
`;
const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
background-color: ${({ theme }) => theme.tag.background.gray};
height: 100%;
width: ${({ theme }) => theme.spacing(1)};
+ border-radius: ${({ theme }) => theme.border.radius.xs};
${({ active, theme }) =>
active &&
@@ -68,6 +73,7 @@ const StyledVisibilityCard = styled(Card)<{ active: boolean }>`
active ? theme.font.color.primary : theme.font.color.light};
border-color: ${({ theme }) => theme.border.color.light};
flex: 1 0 auto;
+ transition: color ${({ theme }) => theme.animation.duration.normal} ease;
`;
const StyledVisibilityCardContent = styled(CardContent)`
@@ -87,45 +93,41 @@ export const CalendarEventRow = ({
}: CalendarEventRowProps) => {
const theme = useTheme();
- const hasEventEnded = calendarEvent.endsAt
- ? isPast(calendarEvent.endsAt)
- : calendarEvent.isFullDay && isPast(endOfDay(calendarEvent.startsAt));
+ const endsAt = getCalendarEventEndDate(calendarEvent);
+ const hasEnded = hasCalendarEventEnded(calendarEvent);
+
+ const startTimeLabel = calendarEvent.isFullDay
+ ? 'All day'
+ : format(calendarEvent.startsAt, 'HH:mm');
+ const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');
return (
- {calendarEvent.isFullDay ? (
- 'All Day'
- ) : (
+ {startTimeLabel}
+ {endTimeLabel && (
<>
- {format(calendarEvent.startsAt, 'HH:mm')}
- {!!calendarEvent.endsAt && (
- <>
-
- {format(calendarEvent.endsAt, 'HH:mm')}
- >
- )}
+
+ {endTimeLabel}
>
)}
{calendarEvent.visibility === 'METADATA' ? (
-
+
Not shared
) : (
-
+
{calendarEvent.title}
)}
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx
index 45ccf37c8..e83adb4ed 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx
@@ -1,41 +1,34 @@
-import { startOfDay } from 'date-fns';
+import { useContext } from 'react';
+import styled from '@emotion/styled';
import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent';
-import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { Card } from '@/ui/layout/card/components/Card';
-import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
-import { sortDesc } from '~/utils/sort';
type CalendarMonthCardProps = {
- calendarEvents: CalendarEvent[];
+ dayTimes: number[];
};
-export const CalendarMonthCard = ({
- calendarEvents,
-}: CalendarMonthCardProps) => {
- const calendarEventsByDayTime = groupArrayItemsBy(
- calendarEvents,
- ({ startsAt }) => startOfDay(startsAt).getTime(),
- );
- const sortedDayTimes = Object.keys(calendarEventsByDayTime)
- .map(Number)
- .sort(sortDesc);
+const StyledCard = styled(Card)`
+ overflow: visible;
+`;
+
+export const CalendarMonthCard = ({ dayTimes }: CalendarMonthCardProps) => {
+ const { calendarEventsByDayTime } = useContext(CalendarContext);
return (
-
- {sortedDayTimes.map((dayTime, index) => {
- const dayCalendarEvents = calendarEventsByDayTime[dayTime];
+
+ {dayTimes.map((dayTime, index) => {
+ const dayCalendarEvents = calendarEventsByDayTime[dayTime] || [];
return (
- !!dayCalendarEvents?.length && (
-
- )
+
);
})}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
new file mode 100644
index 000000000..c5a6e7ee5
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
@@ -0,0 +1,19 @@
+import { createContext } from 'react';
+
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+
+type CalendarContextValue = {
+ calendarEventsByDayTime: Record;
+ currentCalendarEvent: CalendarEvent;
+ getNextCalendarEvent: (
+ calendarEvent: CalendarEvent,
+ ) => CalendarEvent | undefined;
+ updateCurrentCalendarEvent: () => void;
+};
+
+export const CalendarContext = createContext({
+ calendarEventsByDayTime: {},
+ currentCalendarEvent: {} as CalendarEvent,
+ getNextCalendarEvent: () => undefined,
+ updateCurrentCalendarEvent: () => {},
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts
new file mode 100644
index 000000000..3621c942e
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts
@@ -0,0 +1,75 @@
+import { useMemo, useState } from 'react';
+import { getYear, isThisMonth, startOfDay, startOfMonth } from 'date-fns';
+
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { findUpcomingCalendarEvent } from '@/activities/calendar/utils/findUpcomingCalendarEvent';
+import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
+import { isDefined } from '~/utils/isDefined';
+import { sortDesc } from '~/utils/sort';
+
+export const useCalendarEvents = (calendarEvents: CalendarEvent[]) => {
+ const calendarEventsByDayTime = groupArrayItemsBy(
+ calendarEvents,
+ ({ startsAt }) => startOfDay(startsAt).getTime(),
+ );
+
+ const sortedDayTimes = Object.keys(calendarEventsByDayTime)
+ .map(Number)
+ .sort(sortDesc);
+
+ const daysByMonthTime = groupArrayItemsBy(sortedDayTimes, (dayTime) =>
+ startOfMonth(dayTime).getTime(),
+ );
+
+ const sortedMonthTimes = Object.keys(daysByMonthTime)
+ .map(Number)
+ .sort(sortDesc);
+
+ const monthTimesByYear = groupArrayItemsBy(sortedMonthTimes, getYear);
+
+ const getPreviousCalendarEvent = (calendarEvent: CalendarEvent) => {
+ const calendarEventIndex = calendarEvents.indexOf(calendarEvent);
+ return calendarEventIndex < calendarEvents.length - 1
+ ? calendarEvents[calendarEventIndex + 1]
+ : undefined;
+ };
+
+ const getNextCalendarEvent = (calendarEvent: CalendarEvent) => {
+ const calendarEventIndex = calendarEvents.indexOf(calendarEvent);
+ return calendarEventIndex > 0
+ ? calendarEvents[calendarEventIndex - 1]
+ : undefined;
+ };
+
+ const initialUpcomingCalendarEvent = useMemo(
+ () => findUpcomingCalendarEvent(calendarEvents),
+ [calendarEvents],
+ );
+ const lastEventInCalendar = calendarEvents[0];
+
+ const [currentCalendarEvent, setCurrentCalendarEvent] = useState(
+ (initialUpcomingCalendarEvent &&
+ (isThisMonth(initialUpcomingCalendarEvent.startsAt)
+ ? initialUpcomingCalendarEvent
+ : getPreviousCalendarEvent(initialUpcomingCalendarEvent))) ||
+ lastEventInCalendar,
+ );
+
+ const updateCurrentCalendarEvent = () => {
+ const nextCurrentCalendarEvent = getNextCalendarEvent(currentCalendarEvent);
+
+ if (isDefined(nextCurrentCalendarEvent)) {
+ setCurrentCalendarEvent(nextCurrentCalendarEvent);
+ }
+ };
+
+ return {
+ calendarEventsByDayTime,
+ currentCalendarEvent,
+ daysByMonthTime,
+ getNextCalendarEvent,
+ monthTimes: sortedMonthTimes,
+ monthTimesByYear,
+ updateCurrentCalendarEvent,
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts
new file mode 100644
index 000000000..d6d3d556f
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts
@@ -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 = {
+ startsAt: subHours(new Date(), 2),
+ endsAt: subHours(new Date(), 1),
+ isFullDay: false,
+};
+const fullDayPastEvent: Pick = {
+ startsAt: subDays(new Date(), 1),
+ isFullDay: true,
+};
+
+const currentEvent: Pick = {
+ startsAt: addHours(new Date(), 1),
+ endsAt: addHours(new Date(), 2),
+ isFullDay: false,
+};
+const currentFullDayEvent: Pick = {
+ startsAt: startOfDay(new Date()),
+ isFullDay: true,
+};
+
+const futureEvent: Pick = {
+ startsAt: addDays(new Date(), 1),
+ endsAt: addDays(new Date(), 2),
+ isFullDay: false,
+};
+const fullDayFutureEvent: Pick = {
+ 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();
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts
new file mode 100644
index 000000000..0efcf75e4
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts
new file mode 100644
index 000000000..2dcc38876
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts
@@ -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);
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
index c56b434c2..2e0208d6e 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
@@ -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);
});
});
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts b/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts
new file mode 100644
index 000000000..250a45112
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts
@@ -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,
+>(
+ calendarEvents: T[],
+) =>
+ [...calendarEvents]
+ .sort(sortCalendarEventsAsc)
+ .find((calendarEvent) => !hasCalendarEventEnded(calendarEvent));
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts
new file mode 100644
index 000000000..0d25a605b
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts
@@ -0,0 +1,7 @@
+import { endOfDay } from 'date-fns';
+
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+
+export const getCalendarEventEndDate = (
+ calendarEvent: Pick,
+) => calendarEvent.endsAt ?? endOfDay(calendarEvent.startsAt);
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventEnded.ts b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventEnded.ts
new file mode 100644
index 000000000..9800634db
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventEnded.ts
@@ -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,
+) => isPast(getCalendarEventEndDate(calendarEvent));
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts
new file mode 100644
index 000000000..ec7d7a2c5
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts
@@ -0,0 +1,7 @@
+import { isPast } from 'date-fns';
+
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+
+export const hasCalendarEventStarted = (
+ calendarEvent: Pick,
+) => isPast(calendarEvent.startsAt);
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts b/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
index 2619d2887..fc3b77353 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
@@ -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,
- calendarEventB: Pick,
+ calendarEventA: Pick,
+ calendarEventB: Pick,
) => {
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,
- calendarEventB: Pick,
+ calendarEventA: Pick,
+ calendarEventB: Pick,
) => -sortCalendarEventsAsc(calendarEventA, calendarEventB);
diff --git a/packages/twenty-front/src/modules/ui/layout/card/components/CardContent.tsx b/packages/twenty-front/src/modules/ui/layout/card/components/CardContent.tsx
index 7b7fbb148..5c7c5fb94 100644
--- a/packages/twenty-front/src/modules/ui/layout/card/components/CardContent.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/card/components/CardContent.tsx
@@ -1,7 +1,8 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
+import { motion } from 'framer-motion';
-const StyledCardContent = styled.div<{ divider?: boolean }>`
+const StyledCardContent = styled(motion.div)<{ divider?: boolean }>`
background-color: ${({ theme }) => theme.background.secondary};
padding: ${({ theme }) => theme.spacing(4)};