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 a02ed3b73..efd85a128 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
@@ -42,6 +42,7 @@ export const Calendar = () => {
value={{
calendarEventsByDayTime,
currentCalendarEvent,
+ displayCurrentEventCursor: true,
getNextCalendarEvent,
updateCurrentCalendarEvent,
}}
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 8cd98d60e..8523de072 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
@@ -1,14 +1,21 @@
+import { useContext } from 'react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { format } from 'date-fns';
+import { useRecoilValue } from 'recoil';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
+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 { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { IconArrowRight, IconLock } from '@/ui/display/icon';
import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent';
+import { Avatar } from '@/users/components/Avatar';
+import { AvatarGroup } from '@/users/components/AvatarGroup';
+import { isDefined } from '~/utils/isDefined';
type CalendarEventRowProps = {
calendarEvent: CalendarEvent;
@@ -92,6 +99,8 @@ export const CalendarEventRow = ({
className,
}: CalendarEventRowProps) => {
const theme = useTheme();
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const { displayCurrentEventCursor = false } = useContext(CalendarContext);
const endsAt = getCalendarEventEndDate(calendarEvent);
const hasEnded = hasCalendarEventEnded(calendarEvent);
@@ -101,9 +110,13 @@ export const CalendarEventRow = ({
: format(calendarEvent.startsAt, 'HH:mm');
const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');
+ const isCurrentWorkspaceMemberAttending = !!calendarEvent.attendees?.find(
+ ({ workspaceMemberId }) => workspaceMemberId === currentWorkspaceMember?.id,
+ );
+
return (
-
+
{startTimeLabel}
@@ -127,7 +140,27 @@ export const CalendarEventRow = ({
)}
-
+ {!!calendarEvent.attendees?.length && (
+ (
+
+ ))}
+ />
+ )}
+ {displayCurrentEventCursor && (
+
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
index c5a6e7ee5..ff81adcf6 100644
--- a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
@@ -5,6 +5,7 @@ import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
type CalendarContextValue = {
calendarEventsByDayTime: Record;
currentCalendarEvent: CalendarEvent;
+ displayCurrentEventCursor?: boolean;
getNextCalendarEvent: (
calendarEvent: CalendarEvent,
) => CalendarEvent | undefined;
diff --git a/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts b/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
index db48befed..c12c5f19f 100644
--- a/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
@@ -7,4 +7,8 @@ export type CalendarEvent = {
isCanceled?: boolean;
title?: string;
visibility: 'METADATA' | 'SHARE_EVERYTHING';
+ attendees?: {
+ displayName: string;
+ workspaceMemberId?: string;
+ }[];
};
diff --git a/packages/twenty-front/src/modules/users/components/Avatar.tsx b/packages/twenty-front/src/modules/users/components/Avatar.tsx
index d28064bb2..8cc37a71b 100644
--- a/packages/twenty-front/src/modules/users/components/Avatar.tsx
+++ b/packages/twenty-front/src/modules/users/components/Avatar.tsx
@@ -12,7 +12,7 @@ export type AvatarType = 'squared' | 'rounded';
export type AvatarSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type AvatarProps = {
- avatarUrl: string | null | undefined;
+ avatarUrl?: string | null;
className?: string;
size?: AvatarSize;
placeholder: string | undefined;
@@ -23,6 +23,29 @@ export type AvatarProps = {
onClick?: () => void;
};
+const propertiesBySize = {
+ xl: {
+ fontSize: '16px',
+ width: '40px',
+ },
+ lg: {
+ fontSize: '13px',
+ width: '24px',
+ },
+ md: {
+ fontSize: '12px',
+ width: '16px',
+ },
+ sm: {
+ fontSize: '10px',
+ width: '14px',
+ },
+ xs: {
+ fontSize: '8px',
+ width: '12px',
+ },
+};
+
export const StyledAvatar = styled.div<
AvatarProps & { color: string; backgroundColor: string }
>`
@@ -38,54 +61,12 @@ export const StyledAvatar = styled.div<
display: flex;
flex-shrink: 0;
- font-size: ${({ size }) => {
- switch (size) {
- case 'xl':
- return '16px';
- case 'lg':
- return '13px';
- case 'md':
- default:
- return '12px';
- case 'sm':
- return '10px';
- case 'xs':
- return '8px';
- }
- }};
+ font-size: ${({ size = 'md' }) => propertiesBySize[size].fontSize};
font-weight: ${({ theme }) => theme.font.weight.medium};
- height: ${({ size }) => {
- switch (size) {
- case 'xl':
- return '40px';
- case 'lg':
- return '24px';
- case 'md':
- default:
- return '16px';
- case 'sm':
- return '14px';
- case 'xs':
- return '12px';
- }
- }};
+ height: ${({ size = 'md' }) => propertiesBySize[size].width};
justify-content: center;
- width: ${({ size }) => {
- switch (size) {
- case 'xl':
- return '40px';
- case 'lg':
- return '24px';
- case 'md':
- default:
- return '16px';
- case 'sm':
- return '14px';
- case 'xs':
- return '12px';
- }
- }};
+ width: ${({ size = 'md' }) => propertiesBySize[size].width};
&:hover {
box-shadow: ${({ theme, onClick }) =>
diff --git a/packages/twenty-front/src/modules/users/components/AvatarGroup.tsx b/packages/twenty-front/src/modules/users/components/AvatarGroup.tsx
new file mode 100644
index 000000000..6bf437b87
--- /dev/null
+++ b/packages/twenty-front/src/modules/users/components/AvatarGroup.tsx
@@ -0,0 +1,29 @@
+import { ReactNode } from 'react';
+import styled from '@emotion/styled';
+
+export type AvatarGroupProps = {
+ avatars: ReactNode[];
+};
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+`;
+
+const StyledItemContainer = styled.div`
+ margin-right: -3px;
+`;
+
+const MAX_AVATARS_NB = 4;
+
+export const AvatarGroup = ({ avatars }: AvatarGroupProps) => {
+ if (!avatars.length) return null;
+
+ return (
+
+ {avatars.slice(0, MAX_AVATARS_NB).map((avatar, index) => (
+ {avatar}
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/users/components/__stories__/AvatarGroup.stories.tsx b/packages/twenty-front/src/modules/users/components/__stories__/AvatarGroup.stories.tsx
new file mode 100644
index 000000000..0a6676cdc
--- /dev/null
+++ b/packages/twenty-front/src/modules/users/components/__stories__/AvatarGroup.stories.tsx
@@ -0,0 +1,68 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import {
+ Avatar,
+ AvatarProps,
+ AvatarSize,
+ AvatarType,
+} from '@/users/components/Avatar';
+import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
+import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
+import { avatarUrl } from '~/testing/mock-data/users';
+
+import { AvatarGroup, AvatarGroupProps } from '../AvatarGroup';
+
+const makeAvatar = (userName: string, props: Partial = {}) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+);
+
+const getAvatars = (commonProps: Partial = {}) => [
+ makeAvatar('Matthew', { avatarUrl, ...commonProps }),
+ makeAvatar('Sophie', commonProps),
+ makeAvatar('Jane', commonProps),
+ makeAvatar('Lily', commonProps),
+ makeAvatar('John', commonProps),
+];
+
+const meta: Meta<
+ AvatarGroupProps & AvatarProps & { numberOfAvatars?: number }
+> = {
+ title: 'Modules/Users/AvatarGroup',
+ component: AvatarGroup,
+ render: ({ numberOfAvatars = 5, ...args }) => (
+
+ ),
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ decorators: [ComponentDecorator],
+};
+
+export const Catalog: Story = {
+ parameters: {
+ catalog: {
+ dimensions: [
+ {
+ name: 'number of avatars',
+ values: [1, 2, 3, 4, 5],
+ props: (numberOfAvatars: number) => ({ numberOfAvatars }),
+ },
+ {
+ name: 'types',
+ values: ['rounded', 'squared'] as AvatarType[],
+ props: (type: AvatarType) => ({ type }),
+ },
+ {
+ name: 'sizes',
+ values: ['xs', 'sm', 'md', 'lg', 'xl'] as AvatarSize[],
+ props: (size: AvatarSize) => ({ size }),
+ },
+ ],
+ },
+ },
+ decorators: [CatalogDecorator],
+};
diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx
index e65204f01..9d4a537b8 100644
--- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx
+++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx
@@ -1,6 +1,10 @@
+import { addMinutes, endOfDay, min, startOfDay } from 'date-fns';
import { useRecoilValue } from 'recoil';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
+import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
+import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
@@ -27,6 +31,32 @@ export const SettingsAccountsCalendars = () => {
},
});
+ const exampleStartDate = new Date();
+ const exampleEndDate = min([
+ addMinutes(exampleStartDate, 30),
+ endOfDay(exampleStartDate),
+ ]);
+ const exampleDayTime = startOfDay(exampleStartDate).getTime();
+ const exampleCalendarEvent: CalendarEvent = {
+ id: '',
+ attendees: [
+ {
+ displayName: currentWorkspaceMember
+ ? [
+ currentWorkspaceMember.name.firstName,
+ currentWorkspaceMember.name.lastName,
+ ].join(' ')
+ : '',
+ workspaceMemberId: currentWorkspaceMember?.id ?? '',
+ },
+ ],
+ endsAt: exampleEndDate,
+ isFullDay: false,
+ startsAt: exampleStartDate,
+ title: 'Onboarding call',
+ visibility: 'SHARE_EVERYTHING',
+ };
+
return (
@@ -48,13 +78,33 @@ export const SettingsAccountsCalendars = () => {
{/* TODO: retrieve connected accounts data from back-end when the Calendar feature is ready. */}
{!!mockedConnectedAccounts.length && (
-
+ <>
+
+
+
+ undefined,
+ updateCurrentCalendarEvent: () => {},
+ }}
+ >
+
+
+
+ >
)}
diff --git a/packages/twenty-front/src/testing/mock-data/calendar.ts b/packages/twenty-front/src/testing/mock-data/calendar.ts
index 7435ef582..1f1d8c760 100644
--- a/packages/twenty-front/src/testing/mock-data/calendar.ts
+++ b/packages/twenty-front/src/testing/mock-data/calendar.ts
@@ -9,6 +9,11 @@ export const mockedCalendarEvents: CalendarEvent[] = [
isFullDay: false,
startsAt: addDays(new Date().setHours(10, 0), 1),
visibility: 'METADATA',
+ attendees: [
+ { displayName: 'John Doe', workspaceMemberId: 'john-doe' },
+ { displayName: 'Jane Doe', workspaceMemberId: 'jane-doe' },
+ { displayName: 'Tim Apple', workspaceMemberId: 'tim-apple' },
+ ],
},
{
id: '19b32878-a950-4968-9e3b-ce5da514ea41',