From 40b4e9f8e9dd6e68f16e1c3d739f54c9ad85102f Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Mon, 4 Dec 2023 11:37:25 +0100 Subject: [PATCH] Redesign Timeline (#1772) * Timeline redesign for desktop and mobile * Fixed nowrap on desktop --------- Co-authored-by: Lucas Bordeau --- .../timeline/components/TimelineActivity.tsx | 218 ++++++++++-------- .../components/TimelineItemsContainer.tsx | 40 ++-- .../components/TimelingeActivityGroup.tsx | 78 +++++++ .../timeline/utils/groupActivitiesByMonth.ts | 31 +++ .../ui/layout/tab/components/TabList.tsx | 1 + .../src/modules/ui/theme/constants/border.ts | 1 + 6 files changed, 256 insertions(+), 113 deletions(-) create mode 100644 front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx create mode 100644 front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts diff --git a/front/src/modules/activities/timeline/components/TimelineActivity.tsx b/front/src/modules/activities/timeline/components/TimelineActivity.tsx index 48ddd8adf..3c2bd4ceb 100644 --- a/front/src/modules/activities/timeline/components/TimelineActivity.tsx +++ b/front/src/modules/activities/timeline/components/TimelineActivity.tsx @@ -1,41 +1,85 @@ import { Tooltip } from 'react-tooltip'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; -import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask'; import { Activity } from '@/activities/types/Activity'; -import { IconNotes } from '@/ui/display/icon'; -import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; +import { IconCheckbox, IconNotes } from '@/ui/display/icon'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { Avatar } from '@/users/components/Avatar'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { beautifyExactDateTime, beautifyPastDateRelativeToNow, } from '~/utils/date-utils'; -import { TimelineActivityCardFooter } from './TimelineActivityCardFooter'; -import { TimelineActivityTitle } from './TimelineActivityTitle'; +const StyledAvatarContainer = styled.div` + align-items: center; + display: flex; + height: 26px; + justify-content: center; + user-select: none; + width: 26px; + z-index: 2; +`; const StyledIconContainer = styled.div` align-items: center; color: ${({ theme }) => theme.font.color.tertiary}; display: flex; - height: 20px; + height: 16px; justify-content: center; - width: 20px; + text-decoration-line: underline; + width: 16px; `; -const StyledItemTitleContainer = styled.div` +const StyledActivityTitle = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + cursor: pointer; + display: flex; + flex: 1; + font-weight: ${({ theme }) => theme.font.weight.regular}; + overflow: hidden; +`; + +const StyledActivityLink = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + overflow: hidden; + text-decoration-line: underline; + text-overflow: ellipsis; +`; + +const StyledItemContainer = styled.div` align-content: center; align-items: center; color: ${({ theme }) => theme.font.color.tertiary}; display: flex; + flex: 1; gap: ${({ theme }) => theme.spacing(1)}; - height: 20px; span { color: ${({ theme }) => theme.font.color.secondary}; } + overflow: hidden; +`; + +const StyledItemTitleContainer = styled.div` + display: flex; + flex: 1; + flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')}; + gap: ${({ theme }) => theme.spacing(1)}; + overflow: hidden; +`; + +const StyledItemAuthorText = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledItemTitle = styled.div` + display: flex; + flex-flow: row nowrap; + overflow: hidden; `; const StyledItemTitleDate = styled.div` @@ -44,6 +88,7 @@ const StyledItemTitleDate = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(2)}; justify-content: flex-end; + margin-left: auto; `; const StyledVerticalLineContainer = styled.div` @@ -52,7 +97,8 @@ const StyledVerticalLineContainer = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(2)}; justify-content: center; - width: 20px; + width: 26px; + z-index: 2; `; const StyledVerticalLine = styled.div` @@ -62,35 +108,6 @@ const StyledVerticalLine = styled.div` width: 2px; `; -const StyledCardContainer = styled.div` - align-items: center; - cursor: pointer; - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(2)}; - max-width: 100%; - padding: 4px 0px 20px 0px; - width: ${() => (useIsMobile() ? '100%' : '400px')}; -`; - -const StyledCard = styled.div` - align-items: flex-start; - background: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; - width: calc(100% - ${({ theme }) => theme.spacing(4)}); -`; - -const StyledCardContent = styled.div` - align-self: stretch; - color: ${({ theme }) => theme.font.color.secondary}; - margin-top: ${({ theme }) => theme.spacing(2)}; - width: calc(100% - ${({ theme }) => theme.spacing(4)}); -`; - const StyledTooltip = styled(Tooltip)` background-color: ${({ theme }) => theme.background.primary}; @@ -106,16 +123,15 @@ const StyledTooltip = styled(Tooltip)` padding: ${({ theme }) => theme.spacing(2)}; `; -const StyledCardDetailsContainer = styled.div` - padding: ${({ theme }) => theme.spacing(2)}; - width: calc(100% - ${({ theme }) => theme.spacing(4)}); -`; - -const StyledTimelineItemContainer = styled.div` +const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>` align-items: center; align-self: stretch; display: flex; gap: ${({ theme }) => theme.spacing(4)}; + height: ${({ isGap, theme }) => + isGap ? (useIsMobile() ? theme.spacing(6) : theme.spacing(3)) : 'auto'}; + overflow: hidden; + white-space: nowrap; `; type TimelineActivityProps = { @@ -129,66 +145,80 @@ type TimelineActivityProps = { | 'type' | 'comments' | 'dueAt' - > & { author: Pick } & { + > & { author: Pick } & { assignee?: Pick | null; }; + isLastActivity?: boolean; }; -export const TimelineActivity = ({ activity }: TimelineActivityProps) => { +export const TimelineActivity = ({ + activity, + isLastActivity, +}: TimelineActivityProps) => { const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt); const exactCreatedAt = beautifyExactDateTime(activity.createdAt); - const body = JSON.parse( - isNonEmptyString(activity.body) ? activity.body : '{}', - )[0]?.content[0]?.text; - const openActivityRightDrawer = useOpenActivityRightDrawer(); - const { completeTask } = useCompleteTask(activity); + const theme = useTheme(); return ( <> - - - - - - {activity.author.name.firstName + - ' ' + - activity.author.name.lastName} - - created a {activity.type.toLowerCase()} - - - {beautifiedCreatedAt} - - - - - - - - - openActivityRightDrawer(activity.id)}> - - - - {body && } - - - - - + + + + + + + + {activity.author.name.firstName} {activity.author.name.lastName} + + created a {activity.type.toLowerCase()} + + + + {activity.type === 'Note' && ( + + )} + {activity.type === 'Task' && ( + + )} + + {(activity.type === 'Note' || activity.type === 'Task') && ( + openActivityRightDrawer(activity.id)} + > + “ + + {activity.title ?? '(No Title)'} + + “ + + )} + + + + {beautifiedCreatedAt} + + + + {!isLastActivity && ( + + + + + + )} ); }; diff --git a/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx b/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx index ea3d5e54d..47b37d7e0 100644 --- a/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx +++ b/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer'; -import { IconCircleDot } from '@/ui/display/icon'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -import { TimelineActivity } from './TimelineActivity'; +import { groupActivitiesByMonth } from '../utils/groupActivitiesByMonth'; + +import { TimelineActivityGroup } from './TimelingeActivityGroup'; const StyledTimelineContainer = styled.div` align-items: center; @@ -18,15 +17,8 @@ const StyledTimelineContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; justify-content: flex-start; - padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(4)}; -`; - -const StyledStartIcon = styled.div` - align-self: flex-start; - color: ${({ theme }) => theme.font.color.tertiary}; - display: flex; - height: 20px; - width: 20px; + padding: ${({ theme }) => theme.spacing(4)}; + width: calc(100% - ${({ theme }) => theme.spacing(8)}); `; const StyledScrollWrapper = styled(ScrollWrapper)``; @@ -38,16 +30,26 @@ export type TimelineItemsContainerProps = { export const TimelineItemsContainer = ({ activities, }: TimelineItemsContainerProps) => { - const theme = useTheme(); + const groupedActivities = groupActivitiesByMonth(activities); + return ( - {activities.map((activity) => ( - + {groupedActivities.map((group, index) => ( + ))} - - - ); diff --git a/front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx b/front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx new file mode 100644 index 000000000..f405ab2dc --- /dev/null +++ b/front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; + +import { ActivityGroup } from '../utils/groupActivitiesByMonth'; + +import { TimelineActivity } from './TimelineActivity'; + +type TimelineActivityGroupProps = { + group: ActivityGroup; + month: string; + year?: number; +}; + +const StyledActivityGroup = styled.div` + display: flex; + flex-flow: column; + gap: ${({ theme }) => theme.spacing(4)}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + width: 100%; +`; + +const StyledActivityGroupContainer = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; + position: relative; +`; + +const StyledActivityGroupBar = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.xl}; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + position: absolute; + top: 0; + width: 24px; +`; + +const StyledMonthSeperator = styled.div` + align-items: center; + align-self: stretch; + color: ${({ theme }) => theme.font.color.light}; + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; +`; +const StyledMonthSeperatorLine = styled.div` + background: ${({ theme }) => theme.border.color.light}; + border-radius: 50px; + flex: 1 0 0; + height: 1px; +`; + +export const TimelineActivityGroup = ({ + group, + month, + year, +}: TimelineActivityGroupProps) => { + return ( + + + {month} {year} + + + + + {group.items.map((activity, index) => ( + + ))} + + + ); +}; diff --git a/front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts b/front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts new file mode 100644 index 000000000..2dcc43bc7 --- /dev/null +++ b/front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts @@ -0,0 +1,31 @@ +import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer'; + +export interface ActivityGroup { + month: number; + year: number; + items: ActivityForDrawer[]; +} + +export const groupActivitiesByMonth = (activities: ActivityForDrawer[]) => { + const acitivityGroups: ActivityGroup[] = []; + for (const activity of activities) { + const d = new Date(activity.createdAt); + const month = d.getMonth(); + const year = d.getFullYear(); + + const matchingGroup = acitivityGroups.find( + (x) => x.year === year && x.month === month, + ); + if (matchingGroup) { + matchingGroup.items.push(activity); + } else { + acitivityGroups.push({ + year, + month, + items: [activity], + }); + } + } + + return acitivityGroups.sort((a, b) => b.year - a.year || b.month - a.month); +}; diff --git a/front/src/modules/ui/layout/tab/components/TabList.tsx b/front/src/modules/ui/layout/tab/components/TabList.tsx index 28fc254e1..64c675470 100644 --- a/front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/front/src/modules/ui/layout/tab/components/TabList.tsx @@ -28,6 +28,7 @@ const StyledContainer = styled.div` gap: ${({ theme }) => theme.spacing(2)}; height: 40px; padding-left: ${({ theme }) => theme.spacing(2)}; + user-select: none; `; export const TabList = ({ tabs, context }: TabListProps) => { diff --git a/front/src/modules/ui/theme/constants/border.ts b/front/src/modules/ui/theme/constants/border.ts index d98e2b295..48895b949 100644 --- a/front/src/modules/ui/theme/constants/border.ts +++ b/front/src/modules/ui/theme/constants/border.ts @@ -5,6 +5,7 @@ const common = { xs: '2px', sm: '4px', md: '8px', + xl: '20px', rounded: '100%', }, };