feat: add event rows to Show Page Calendar tab (#4319)
* feat: add event rows to Show Page Calendar tab Closes #4287 * refactor: use time as events group key instead of ISO string for easier sorting * feat: implement data model changes * refactor: improve sorting
This commit is contained in:
@ -0,0 +1,47 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { startOfMonth } from 'date-fns';
|
||||
|
||||
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
|
||||
import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents';
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{sortedMonthTimes.map((monthTime) => {
|
||||
const monthCalendarEvents = calendarEventsByMonthTime[monthTime];
|
||||
|
||||
return (
|
||||
!!monthCalendarEvents?.length && (
|
||||
<CalendarMonthCard
|
||||
key={monthTime}
|
||||
calendarEvents={monthCalendarEvents}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { endOfDay, format, isPast } from 'date-fns';
|
||||
|
||||
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
type CalendarDayCardContentProps = {
|
||||
calendarEvents: CalendarEvent[];
|
||||
divider?: boolean;
|
||||
};
|
||||
|
||||
const StyledCardContent = styled(CardContent)<{ active: boolean }>`
|
||||
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`
|
||||
text-align: center;
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledWeekDay = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xxs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledDateDay = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledEvents = styled.div`
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledEventRow = styled(CalendarEventRow)`
|
||||
flex: 1 0 auto;
|
||||
`;
|
||||
|
||||
export const CalendarDayCardContent = ({
|
||||
calendarEvents,
|
||||
divider,
|
||||
}: CalendarDayCardContentProps) => {
|
||||
const endOfDayDate = endOfDay(calendarEvents[0].startsAt);
|
||||
const isPastDay = isPast(endOfDayDate);
|
||||
|
||||
return (
|
||||
<StyledCardContent active={!isPastDay} divider={divider}>
|
||||
<StyledDayContainer>
|
||||
<StyledWeekDay>{format(endOfDayDate, 'EE')}</StyledWeekDay>
|
||||
<StyledDateDay>{format(endOfDayDate, 'dd')}</StyledDateDay>
|
||||
</StyledDayContainer>
|
||||
<StyledEvents>
|
||||
{calendarEvents.map((calendarEvent) => (
|
||||
<StyledEventRow
|
||||
key={calendarEvent.id}
|
||||
calendarEvent={calendarEvent}
|
||||
/>
|
||||
))}
|
||||
</StyledEvents>
|
||||
</StyledCardContent>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,131 @@
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { endOfDay, format, isPast } from 'date-fns';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { IconArrowRight, IconLock } from '@/ui/display/icon';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
type CalendarEventRowProps = {
|
||||
calendarEvent: CalendarEvent;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
|
||||
background-color: ${({ theme }) => theme.tag.background.gray};
|
||||
height: 100%;
|
||||
width: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
${({ active, theme }) =>
|
||||
active &&
|
||||
css`
|
||||
background-color: ${theme.tag.background.red};
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledLabels = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
flex: 1 0 auto;
|
||||
`;
|
||||
|
||||
const StyledTime = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
width: ${({ theme }) => theme.spacing(26)};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div<{ active: boolean; canceled: boolean }>`
|
||||
flex: 1 0 auto;
|
||||
|
||||
${({ theme, active }) =>
|
||||
active &&
|
||||
css`
|
||||
color: ${theme.font.color.primary};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
`}
|
||||
|
||||
${({ canceled }) =>
|
||||
canceled &&
|
||||
css`
|
||||
text-decoration: line-through;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledVisibilityCard = styled(Card)<{ active: boolean }>`
|
||||
color: ${({ active, theme }) =>
|
||||
active ? theme.font.color.primary : theme.font.color.light};
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
flex: 1 0 auto;
|
||||
`;
|
||||
|
||||
const StyledVisibilityCardContent = styled(CardContent)`
|
||||
align-items: center;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(0, 1)};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
`;
|
||||
|
||||
export const CalendarEventRow = ({
|
||||
calendarEvent,
|
||||
className,
|
||||
}: CalendarEventRowProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const hasEventEnded = calendarEvent.endsAt
|
||||
? isPast(calendarEvent.endsAt)
|
||||
: calendarEvent.isFullDay && isPast(endOfDay(calendarEvent.startsAt));
|
||||
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<StyledAttendanceIndicator />
|
||||
<StyledLabels>
|
||||
<StyledTime>
|
||||
{calendarEvent.isFullDay ? (
|
||||
'All Day'
|
||||
) : (
|
||||
<>
|
||||
{format(calendarEvent.startsAt, 'HH:mm')}
|
||||
{!!calendarEvent.endsAt && (
|
||||
<>
|
||||
<IconArrowRight size={theme.icon.size.sm} />
|
||||
{format(calendarEvent.endsAt, 'HH:mm')}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledTime>
|
||||
{calendarEvent.visibility === 'METADATA' ? (
|
||||
<StyledVisibilityCard active={!hasEventEnded}>
|
||||
<StyledVisibilityCardContent>
|
||||
<IconLock size={theme.icon.size.sm} />
|
||||
Not shared
|
||||
</StyledVisibilityCardContent>
|
||||
</StyledVisibilityCard>
|
||||
) : (
|
||||
<StyledTitle
|
||||
active={!hasEventEnded}
|
||||
canceled={!!calendarEvent.isCanceled}
|
||||
>
|
||||
{calendarEvent.title}
|
||||
</StyledTitle>
|
||||
)}
|
||||
</StyledLabels>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import { startOfDay } from 'date-fns';
|
||||
|
||||
import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent';
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
||||
import { sortDesc } from '~/utils/sort';
|
||||
|
||||
type CalendarMonthCardProps = {
|
||||
calendarEvents: CalendarEvent[];
|
||||
};
|
||||
|
||||
export const CalendarMonthCard = ({
|
||||
calendarEvents,
|
||||
}: CalendarMonthCardProps) => {
|
||||
const calendarEventsByDayTime = groupArrayItemsBy(
|
||||
calendarEvents,
|
||||
({ startsAt }) => startOfDay(startsAt).getTime(),
|
||||
);
|
||||
const sortedDayTimes = Object.keys(calendarEventsByDayTime)
|
||||
.map(Number)
|
||||
.sort(sortDesc);
|
||||
|
||||
return (
|
||||
<Card fullWidth>
|
||||
{sortedDayTimes.map((dayTime, index) => {
|
||||
const dayCalendarEvents = calendarEventsByDayTime[dayTime];
|
||||
|
||||
return (
|
||||
!!dayCalendarEvents?.length && (
|
||||
<CalendarDayCardContent
|
||||
key={dayTime}
|
||||
calendarEvents={dayCalendarEvents}
|
||||
divider={index < sortedDayTimes.length - 1}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
const meta: Meta<typeof Calendar> = {
|
||||
title: 'Modules/Activities/Calendar/Calendar',
|
||||
component: Calendar,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
container: { width: 728 },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Calendar>;
|
||||
|
||||
export const Default: Story = {};
|
||||
Reference in New Issue
Block a user