Lucas/t 366 on comment drawer when i have comments on the selected (#201)

* Fixed right drawer width and shared in theme

* Added date packages and tooltip

* Added date utils and tests

* Added comment thread components

* Fixed comment chip

* Fix from rebase

* Fix from rebase

* Fix margin right

* Fixed CSS and graphql
This commit is contained in:
Lucas Bordeau
2023-06-07 12:48:44 +02:00
committed by GitHub
parent b1bf050936
commit 5e2673a2a4
30 changed files with 688 additions and 77 deletions

View File

@ -4,7 +4,7 @@ import { CommentChip, CommentChipProps } from './CommentChip';
const StyledCellWrapper = styled.div`
position: relative;
right: 38px;
right: 34px;
top: -13px;
width: 0;
height: 0;

View File

@ -9,18 +9,18 @@ export type CommentChipProps = {
const StyledChip = styled.div`
height: 26px;
min-width: 34px;
width: fit-content;
padding-left: 2px;
padding-right: 2px;
padding-left: 4px;
padding-right: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: 2px;
gap: 4px;
background: ${(props) => props.theme.secondaryBackgroundTransparent};
background: ${(props) => props.theme.primaryBackgroundTransparent};
backdrop-filter: blur(6px);
border-radius: ${(props) => props.theme.borderRadius};
@ -53,7 +53,7 @@ export function CommentChip({ count, onClick }: CommentChipProps) {
return (
<StyledChip data-testid="comment-chip" onClick={onClick}>
<StyledCount>{formattedCount}</StyledCount>
<IconComment size={12} />
<IconComment size={16} />
</StyledChip>
);
}

View File

@ -0,0 +1,80 @@
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { UserAvatar } from '@/users/components/UserAvatar';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
type OwnProps = {
avatarUrl: string | null | undefined;
username: string;
createdAt: Date;
};
const StyledContainer = styled.div`
display: flex;
align-items: center;
flex-direction: row;
justify-content: flex-start;
padding: ${(props) => props.theme.spacing(1)};
gap: ${(props) => props.theme.spacing(1)};
`;
const StyledName = styled.div`
font-size: 13px;
font-weight: 400;
color: ${(props) => props.theme.text80};
`;
const StyledDate = styled.div`
font-size: 12px;
font-weight: 400;
color: ${(props) => props.theme.text30};
padding-top: 1.5px;
margin-left: ${(props) => props.theme.spacing(1)};
`;
const StyledTooltip = styled(Tooltip)`
padding: 8px;
`;
export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(createdAt);
const exactCreatedAt = beautifyExactDate(createdAt);
const showDate = beautifiedCreatedAt !== '';
const capitalizedFirstUsernameLetter =
username !== '' ? username.toLocaleUpperCase()[0] : '';
return (
<StyledContainer>
<UserAvatar
avatarUrl={avatarUrl}
size={16}
placeholderLetter={capitalizedFirstUsernameLetter}
/>
<StyledName>{username}</StyledName>
{showDate && (
<>
<StyledDate className="comment-created-at">
{beautifiedCreatedAt}
</StyledDate>
<StyledTooltip
anchorSelect=".comment-created-at"
content={exactCreatedAt}
clickable
noArrow
/>
</>
)}
</StyledContainer>
);
}

View File

@ -1,27 +1,37 @@
import styled from '@emotion/styled';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { CommentTextInput } from './CommentTextInput';
import { CommentThreadItem } from './CommentThreadItem';
type OwnProps = {
commentThread: CommentThreadForDrawer;
};
const StyledContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
gap: ${(props) => props.theme.spacing(4)};
padding: ${(props) => props.theme.spacing(2)};
`;
export function CommentThread({ commentThread }: OwnProps) {
function handleSendComment(text: string) {
console.log(text);
}
return (
<div>
<StyledContainer>
{commentThread.comments?.map((comment) => (
<div key={comment.id}>
<div>
{comment.author?.displayName} - {comment.createdAt}
</div>
<div>{comment.body}</div>
</div>
<CommentThreadItem key={comment.id} comment={comment} />
))}
<CommentTextInput onSend={handleSendComment} />
</div>
</StyledContainer>
);
}

View File

@ -0,0 +1,40 @@
import styled from '@emotion/styled';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { CommentHeader } from './CommentHeader';
type OwnProps = {
comment: CommentForDrawer;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: ${(props) => props.theme.spacing(1)};
`;
const StyledCommentBody = styled.div`
font-size: ${(props) => props.theme.fontSizeMedium};
line-height: 19.5px;
text-align: left;
padding-left: 24px;
color: ${(props) => props.theme.text60};
`;
export function CommentThreadItem({ comment }: OwnProps) {
return (
<StyledContainer>
<CommentHeader
avatarUrl={comment.author.avatarUrl}
username={comment.author.displayName}
createdAt={comment.createdAt}
/>
<StyledCommentBody>{comment.body}</StyledCommentBody>
</StyledContainer>
);
}

View File

@ -7,7 +7,7 @@ import { CellCommentChip } from '../CellCommentChip';
import { CommentChip } from '../CommentChip';
const meta: Meta<typeof CellCommentChip> = {
title: 'Components/CellCommentChip',
title: 'Components/Comments/CellCommentChip',
component: CellCommentChip,
};

View File

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon';
import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentHeader } from '../CommentHeader';
const meta: Meta<typeof CommentHeader> = {
title: 'Components/Comments/CommentHeader',
component: CommentHeader,
};
export default meta;
type Story = StoryObj<typeof CommentHeader>;
const mockUser = mockedUsersData[0];
export const Default: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
/>,
),
};
export const FewDaysAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ days: 2 }).toJSDate()}
/>,
),
};
export const FewMonthsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ months: 2 }).toJSDate()}
/>,
),
};
export const FewYearsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ years: 2 }).toJSDate()}
/>,
),
};
export const WithoutAvatar: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
/>,
),
};

View File

@ -6,7 +6,7 @@ import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentTextInput } from '../CommentTextInput';
const meta: Meta<typeof CommentTextInput> = {
title: 'Components/CommentTextInput',
title: 'Components/Comments/CommentTextInput',
component: CommentTextInput,
argTypes: {
onSend: {

View File

@ -1,12 +1,12 @@
import { gql } from '@apollo/client';
import {
useGetCompanyCountsQuery,
useGetPeopleCountsQuery,
useGetCompanyCommentsCountQuery,
useGetPeopleCommentsCountQuery,
} from '../../../generated/graphql';
export const GET_COMPANY_COMMENT_COUNT = gql`
query GetCompanyCounts($where: CompanyWhereInput) {
query GetCompanyCommentsCount($where: CompanyWhereInput) {
companies: findManyCompany(where: $where) {
commentsCount: _commentCount
}
@ -14,14 +14,14 @@ export const GET_COMPANY_COMMENT_COUNT = gql`
`;
export const useCompanyCommentsCountQuery = (companyId: string) => {
const { data, ...rest } = useGetCompanyCountsQuery({
const { data, ...rest } = useGetCompanyCommentsCountQuery({
variables: { where: { id: { equals: companyId } } },
});
return { ...rest, data: data?.companies[0].commentsCount };
};
export const GET_PEOPLE_COMMENT_COUNT = gql`
query GetPeopleCounts($where: PersonWhereInput) {
query GetPeopleCommentsCount($where: PersonWhereInput) {
people: findManyPerson(where: $where) {
commentsCount: _commentCount
}
@ -29,7 +29,7 @@ export const GET_PEOPLE_COMMENT_COUNT = gql`
`;
export const usePeopleCommentsCountQuery = (personId: string) => {
const { data, ...rest } = useGetPeopleCountsQuery({
const { data, ...rest } = useGetPeopleCommentsCountQuery({
variables: { where: { id: { equals: personId } } },
});
return { ...rest, data: data?.people[0].commentsCount };

View File

@ -0,0 +1,5 @@
import { CommentThreadForDrawer } from './CommentThreadForDrawer';
export type CommentForDrawer = NonNullable<
CommentThreadForDrawer['comments']
>[0];

View File

@ -1,9 +1,9 @@
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { useCompanyCommentsCountQuery } from '@/comments/services';
import EditableChip from '@/ui/components/editable-cell/types/EditableChip';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import { CellCommentChip } from '../../comments/components/comments/CellCommentChip';
import { useCompanyCommentsCountQuery } from '../../comments/services';
import { Company } from '../interfaces/company.interface';
import { updateCompany } from '../services';
@ -16,7 +16,10 @@ type OwnProps = {
export function CompanyEditableNameChipCell({ company }: OwnProps) {
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleCommentClick() {
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
event.stopPropagation();
openCommentRightDrawer([
{
type: 'Company',
@ -27,6 +30,8 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
const commentCount = useCompanyCommentsCountQuery(company.id);
const displayCommentCount = !commentCount.loading;
return (
<EditableChip
value={company.name || ''}
@ -40,9 +45,9 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
}}
ChipComponent={CompanyChip}
rightEndContents={[
commentCount.loading ? null : (
displayCommentCount && (
<CellCommentChip
count={commentCount.data || 0}
count={commentCount.data ?? 0}
onClick={handleCommentClick}
/>
),

View File

@ -21,6 +21,7 @@ describe('Company mappers', () => {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
__typename: 'User',
},
pipes: [
@ -47,6 +48,7 @@ describe('Company mappers', () => {
__typename: 'users',
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com',
avatarUrl: 'https://example.com/avatar.png',
displayName: 'John Doe',
workspaceMember: undefined,
},
@ -67,6 +69,7 @@ describe('Company mappers', () => {
accountOwner: {
id: '522d4ec4-c46b-4360-a0a7-df8df170be81',
email: 'john@example.com',
avatarUrl: 'https://example.com/avatar.png',
displayName: 'John Doe',
__typename: 'users',
},

View File

@ -2,9 +2,9 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText';
import { useOpenCommentRightDrawer } from '../../comments/hooks/useOpenCommentRightDrawer';
import { usePeopleCommentsCountQuery } from '../../comments/services';
import { PersonChip } from './PersonChip';
@ -31,6 +31,7 @@ export function EditablePeopleFullName({
}: OwnProps) {
const [firstnameValue, setFirstnameValue] = useState(firstname);
const [lastnameValue, setLastnameValue] = useState(lastname);
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleDoubleTextChange(
firstValue: string,
@ -42,14 +43,13 @@ export function EditablePeopleFullName({
onChange(firstValue, secondValue);
}
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
event.stopPropagation();
openCommentRightDrawer([
{
type: 'Company',
type: 'Person',
id: personId,
},
]);
@ -57,6 +57,8 @@ export function EditablePeopleFullName({
const commentCount = usePeopleCommentsCountQuery(personId);
const displayCommentCount = !commentCount.loading;
return (
<EditableDoubleText
firstValue={firstnameValue}
@ -69,9 +71,9 @@ export function EditablePeopleFullName({
<StyledDiv>
<PersonChip name={firstname + ' ' + lastname} />
</StyledDiv>
{commentCount.loading ? null : (
{displayCommentCount && (
<CellCommentChip
count={commentCount.data || 0}
count={commentCount.data ?? 0}
onClick={handleCommentClick}
/>
)}

View File

@ -30,10 +30,9 @@ const MainContainer = styled.div`
background: ${(props) => props.theme.noisyBackground};
padding-right: ${(props) => props.theme.spacing(3)};
padding-bottom: ${(props) => props.theme.spacing(3)};
gap: ${(props) => props.theme.spacing(2)};
`;
const RIGHT_DRAWER_WIDTH = '300px';
type LeftContainerProps = {
isRightDrawerOpen?: boolean;
};
@ -41,7 +40,11 @@ type LeftContainerProps = {
const LeftContainer = styled.div<LeftContainerProps>`
display: flex;
width: calc(
100% - ${(props) => (props.isRightDrawerOpen ? RIGHT_DRAWER_WIDTH : '0px')}
100% -
${(props) =>
props.isRightDrawerOpen
? `${props.theme.rightDrawerWidth} - ${props.theme.spacing(2)}`
: '0px'}
);
position: relative;
`;

View File

@ -12,8 +12,7 @@ import { RightDrawerRouter } from './RightDrawerRouter';
const StyledRightDrawer = styled.div`
display: flex;
flex-direction: row;
width: 300px;
margin-left: ${(props) => props.theme.spacing(2)};
width: ${(props) => props.theme.rightDrawerWidth};
`;
export function RightDrawer() {

View File

@ -3,6 +3,4 @@ import styled from '@emotion/styled';
export const RightDrawerBody = styled.div`
display: flex;
flex-direction: column;
padding: 8px;
`;

View File

@ -23,6 +23,8 @@ const commonTheme = {
},
borderRadius: '4px',
rightDrawerWidth: '300px',
};
const lightThemeSpecific = {
@ -38,6 +40,7 @@ const lightThemeSpecific = {
purpleBackground: '#e0e0ff',
yellowBackground: '#fff2e7',
primaryBackgroundTransparent: 'rgba(255, 255, 255, 0.8)',
secondaryBackgroundTransparent: 'rgba(252, 252, 252, 0.8)',
primaryBorder: 'rgba(0, 0, 0, 0.08)',
@ -77,6 +80,7 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
purpleBackground: '#1111b7',
yellowBackground: '#cc660a',
primaryBackgroundTransparent: 'rgba(20, 20, 20, 0.8)',
secondaryBackgroundTransparent: 'rgba(23, 23, 23, 0.8)',
clickableElementBackgroundHover: 'rgba(0, 0, 0, 0.04)',

View File

@ -0,0 +1,61 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
type OwnProps = {
avatarUrl: string | null | undefined;
size: number;
placeholderLetter: string;
};
export const StyledUserAvatar = styled.div<Omit<OwnProps, 'placeholderLetter'>>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
background-image: url(${(props) =>
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
background-color: ${(props) =>
!isNonEmptyString(props.avatarUrl)
? props.theme.tertiaryBackground
: 'none'};
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
`;
type StyledPlaceholderLetterProps = {
size: number;
};
export const StyledPlaceholderLetter = styled.div<StyledPlaceholderLetterProps>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.text80};
`;
export function UserAvatar({ avatarUrl, size, placeholderLetter }: OwnProps) {
const noAvatarUrl = !isNonEmptyString(avatarUrl);
return (
<StyledUserAvatar avatarUrl={avatarUrl} size={size}>
{noAvatarUrl && (
<StyledPlaceholderLetter size={size}>
{placeholderLetter}
</StyledPlaceholderLetter>
)}
</StyledUserAvatar>
);
}

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { UserAvatar } from '../UserAvatar';
const meta: Meta<typeof UserAvatar> = {
title: 'Components/User/UserAvatar',
component: UserAvatar,
};
export default meta;
type Story = StoryObj<typeof UserAvatar>;
const avatarUrl =
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
export const Size40: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={40} placeholderLetter="L" />,
),
};
export const Size32: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={32} placeholderLetter="L" />,
),
};
export const Size16: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={16} placeholderLetter="L" />,
),
};
export const NoAvatarPicture: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={''} size={16} placeholderLetter="L" />,
),
};

View File

@ -13,6 +13,7 @@ describe('User mappers', () => {
const graphQLUser = {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
email: 'john.doe@gmail.com',
workspaceMember: {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e88',
@ -31,6 +32,7 @@ describe('User mappers', () => {
__typename: 'users',
id: graphQLUser.id,
displayName: graphQLUser.displayName,
avatarUrl: graphQLUser.avatarUrl,
email: graphQLUser.email,
workspaceMember: {
id: graphQLUser.workspaceMember.id,
@ -51,6 +53,7 @@ describe('User mappers', () => {
__typename: 'users',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
email: 'john.doe@gmail.com',
workspaceMember: {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e88',
@ -65,6 +68,7 @@ describe('User mappers', () => {
expect(graphQLUser).toStrictEqual({
id: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
email: user.email,
workspaceMemberId: user.workspaceMember.id,
__typename: 'users',

View File

@ -9,6 +9,7 @@ export interface User {
id: string;
email?: string;
displayName?: string;
avatarUrl?: string;
workspaceMember?: WorkspaceMember | null;
}
@ -17,6 +18,7 @@ export type GraphqlQueryUser = {
email?: string;
displayName?: string;
workspaceMember?: GraphqlQueryWorkspaceMember | null;
avatarUrl?: string;
__typename?: string;
};
@ -24,6 +26,7 @@ export type GraphqlMutationUser = {
id: string;
email?: string;
displayName?: string;
avatarUrl?: string;
workspaceMemberId?: string;
__typename?: string;
};
@ -33,6 +36,7 @@ export const mapToUser = (user: GraphqlQueryUser): User => ({
id: user.id,
email: user.email,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
workspaceMember: user.workspaceMember
? mapToWorkspaceMember(user.workspaceMember)
: user.workspaceMember,
@ -42,6 +46,7 @@ export const mapToGqlUser = (user: User): GraphqlMutationUser => ({
id: user.id,
email: user.email,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
workspaceMemberId: user.workspaceMember?.id,
__typename: 'users',
});

View File

@ -0,0 +1,155 @@
import { formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import { logError } from '@/utils/logs/logError';
import {
beautifyExactDate,
beautifyPastDateAbsolute,
beautifyPastDateRelativeToNow,
DEFAULT_DATE_LOCALE,
parseDate,
} from '../date-utils';
jest.mock('@/utils/logs/logError');
describe('beautifyExactDate', () => {
it('should return the correct relative date', () => {
const mockDate = '2023-01-01T12:13:24';
const actualDate = new Date(mockDate);
const expected = DateTime.fromJSDate(actualDate)
.toUTC()
.setLocale(DEFAULT_DATE_LOCALE)
.toFormat('DD · TT');
const result = beautifyExactDate(mockDate);
expect(result).toEqual(expected);
});
});
describe('parseDate', () => {
it('should log an error and return empty string when passed an invalid date string', () => {
expect(() => {
parseDate('invalid-date-string');
}).toThrow(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
});
it('should log an error and return empty string when passed NaN', () => {
expect(() => {
parseDate(NaN);
}).toThrow(Error('Invalid date passed to formatPastDate: "NaN"'));
});
it('should log an error and return empty string when passed invalid Date object', () => {
expect(() => {
parseDate(new Date(NaN));
}).toThrow(Error('Invalid date passed to formatPastDate: "Invalid Date"'));
});
});
describe('beautifyPastDateRelativeToNow', () => {
it('should return the correct relative date', () => {
const mockDate = '2023-01-01';
const actualDate = new Date(mockDate);
const expected = formatDistanceToNow(actualDate);
const result = beautifyPastDateRelativeToNow(mockDate);
expect(result).toEqual(expected);
});
it('should log an error and return empty string when passed an invalid date string', () => {
const result = beautifyPastDateRelativeToNow('invalid-date-string');
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed NaN', () => {
const result = beautifyPastDateRelativeToNow(NaN);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "NaN"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed invalid Date object', () => {
const result = beautifyPastDateRelativeToNow(
new Date('invalid-date-asdasd'),
);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "Invalid Date"'),
);
expect(result).toEqual('');
});
});
describe('beautifyPastDateAbsolute', () => {
it('should log an error and return empty string when passed an invalid date string', () => {
const result = beautifyPastDateAbsolute('invalid-date-string');
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed NaN', () => {
const result = beautifyPastDateAbsolute(NaN);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "NaN"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed invalid Date object', () => {
const result = beautifyPastDateAbsolute(new Date(NaN));
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "Invalid Date"'),
);
expect(result).toEqual('');
});
it('should return the correct format when the date difference is less than 24 hours', () => {
const now = DateTime.local();
const pastDate = now.minus({ hours: 23 });
const expected = pastDate.toFormat('HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is less than 7 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 6 });
const expected = pastDate.toFormat('cccc - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is less than 365 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 364 });
const expected = pastDate.toFormat('MMMM d - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is more than 365 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 366 });
const expected = pastDate.toFormat('dd/MM/yyyy - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
});

View File

@ -0,0 +1,75 @@
import { formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import { logError } from '../logs/logError';
export const DEFAULT_DATE_LOCALE = 'en-EN';
export function parseDate(dateToParse: Date | string | number) {
let formattedDate: DateTime | null = null;
if (!dateToParse) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
} else if (typeof dateToParse === 'string') {
formattedDate = DateTime.fromISO(dateToParse);
} else if (dateToParse instanceof Date) {
formattedDate = DateTime.fromJSDate(dateToParse);
} else if (typeof dateToParse === 'number') {
formattedDate = DateTime.fromMillis(dateToParse);
}
if (!formattedDate) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
}
if (!formattedDate.isValid) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
}
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
}
export function beautifyExactDate(dateToBeautify: Date | string | number) {
try {
const parsedDate = parseDate(dateToBeautify);
return parsedDate.toFormat('DD · TT');
} catch (error) {
logError(error);
return '';
}
}
export function beautifyPastDateRelativeToNow(
pastDate: Date | string | number,
) {
try {
const parsedDate = parseDate(pastDate);
return formatDistanceToNow(parsedDate.toJSDate());
} catch (error) {
logError(error);
return '';
}
}
export function beautifyPastDateAbsolute(pastDate: Date | string | number) {
try {
const parsedPastDate = parseDate(pastDate);
const hoursDiff = parsedPastDate.diffNow('hours').negate().hours;
if (hoursDiff <= 24) {
return parsedPastDate.toFormat('HH:mm');
} else if (hoursDiff <= 7 * 24) {
return parsedPastDate.toFormat('cccc - HH:mm');
} else if (hoursDiff <= 365 * 24) {
return parsedPastDate.toFormat('MMMM d - HH:mm');
} else {
return parsedPastDate.toFormat('dd/MM/yyyy - HH:mm');
}
} catch (error) {
logError(error);
return '';
}
}

View File

@ -0,0 +1,3 @@
export function logError(message: any) {
console.error(message);
}