diff --git a/packages/components/src/CommentItem/CommentItem.tsx b/packages/components/src/CommentItem/CommentItem.tsx index f8f938a68f..5beb7cc60c 100644 --- a/packages/components/src/CommentItem/CommentItem.tsx +++ b/packages/components/src/CommentItem/CommentItem.tsx @@ -1,10 +1,10 @@ import { createRef, useEffect, useState } from 'react' -import { format, formatDistanceToNow } from 'date-fns' import { Avatar, Box, Flex, Text } from 'theme-ui' import defaultProfileImage from '../../assets/images/default_member.svg' import { Button } from '../Button/Button' import { ConfirmModal } from '../ConfirmModal/ConfirmModal' +import { DisplayDate } from '../DisplayDate/DisplayDate' import { EditComment } from '../EditComment/EditComment' import { LinkifyText } from '../LinkifyText/LinkifyText' import { Modal } from '../Modal/Modal' @@ -23,20 +23,6 @@ export interface IProps { isReply: boolean } -const formatDate = (d: string | undefined): string => { - if (!d) { - return '' - } - return format(new Date(d), 'dd MMMM yyyy h:mm a') -} - -const relativeDateFormat = (d: string | undefined): string => { - if (!d) { - return '' - } - return formatDistanceToNow(new Date(d), { addSuffix: true }) -} - export const CommentItem = (props: IProps) => { const textRef = createRef() const [showEditModal, setShowEditModal] = useState(false) @@ -66,8 +52,6 @@ export const CommentItem = (props: IProps) => { isSupporter: !!isUserSupporter, } - const date = formatDate(_edited || _created) - const relativeDate = relativeDateFormat(_edited || _created) const maxHeight = isShowMore ? 'max-content' : '128px' const item = isReply ? 'ReplyItem' : 'CommentItem' @@ -132,11 +116,9 @@ export const CommentItem = (props: IProps) => { }} > - {_edited && ( - (Edited) - )} - - {relativeDate} + + {_edited && 'Edited '} + diff --git a/packages/components/src/DisplayDate/DisplayDate.stories.tsx b/packages/components/src/DisplayDate/DisplayDate.stories.tsx new file mode 100644 index 0000000000..10066249dc --- /dev/null +++ b/packages/components/src/DisplayDate/DisplayDate.stories.tsx @@ -0,0 +1,23 @@ +import { subMonths } from 'date-fns' + +import { DisplayDate } from './DisplayDate' + +import type { Meta, StoryFn } from '@storybook/react' + +export default { + /* 👇 The title prop is optional. + * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Components/DisplayDate', + component: DisplayDate, +} as Meta + +export const Default: StoryFn = () => { + return +} + +export const TwoMonthsAGo: StoryFn = () => { + const twoMonthsAGo = subMonths(new Date(), 2) + return +} diff --git a/packages/components/src/DisplayDate/DisplayDate.test.tsx b/packages/components/src/DisplayDate/DisplayDate.test.tsx new file mode 100644 index 0000000000..99eb22d290 --- /dev/null +++ b/packages/components/src/DisplayDate/DisplayDate.test.tsx @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom/vitest' + +import { describe, expect, it } from 'vitest' + +import { render } from '../test/utils' +import { Default, TwoMonthsAGo } from './DisplayDate.stories' + +import type { IProps } from './DisplayDate' + +describe('DisplayDate', () => { + it('renders correctly current date', () => { + const { getByText } = render() + + expect(getByText('less than a minute ago')).toBeInTheDocument() + }) + + it('renders correctly when two months ago', () => { + const { getByText } = render( + , + ) + + expect(getByText('2 months ago')).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/DisplayDate/DisplayDate.tsx b/packages/components/src/DisplayDate/DisplayDate.tsx new file mode 100644 index 0000000000..fcffa5ff4f --- /dev/null +++ b/packages/components/src/DisplayDate/DisplayDate.tsx @@ -0,0 +1,30 @@ +import { format, formatDistanceToNow } from 'date-fns' +import { Text } from 'theme-ui' + +type DateType = string | number | Date + +export interface IProps { + date?: DateType +} + +const formatDateTime = (date: DateType | undefined) => { + if (!date) { + return '' + } + + return format(new Date(date), 'dd-MM-yyyy HH:mm') +} + +const relativeDateFormat = (d: DateType | undefined): string => { + if (!d) { + return '' + } + return formatDistanceToNow(new Date(d), { addSuffix: true }) +} + +export const DisplayDate = ({ date }: IProps) => { + const formattedDate = formatDateTime(date) + const relativeDate = relativeDateFormat(date) + + return {relativeDate} +} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index dd676714ba..daac4d9d34 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -18,6 +18,7 @@ export { ConfirmModal } from './ConfirmModal/ConfirmModal' export { CreateComment } from './CreateComment/CreateComment' export { DiscussionContainer } from './DiscussionContainer/DiscussionContainer' export { DiscussionTitle } from './DiscussionTitle/DiscussionTitle' +export { DisplayDate } from './DisplayDate/DisplayDate' export { DonationRequest } from './DonationRequest/DonationRequest' export { DonationRequestModal } from './DonationRequestModal/DonationRequestModal' export { DownloadButton } from './DownloadButton/DownloadButton' diff --git a/packages/cypress/src/integration/howto/read.spec.ts b/packages/cypress/src/integration/howto/read.spec.ts index cdf0756a4e..1f80d4f8a4 100644 --- a/packages/cypress/src/integration/howto/read.spec.ts +++ b/packages/cypress/src/integration/howto/read.spec.ts @@ -65,7 +65,7 @@ describe('[How To]', () => { cy.title().should('eq', `${howto.title} - How-to - Community Platform`) cy.get('[data-cy=how-to-basis]').then(($summary) => { expect($summary).to.contain('howto_creator', 'Author') - expect($summary).to.contain('Last update on', 'Edit') + expect($summary).to.contain('Last update', 'Edit') expect($summary).to.contain('Make an interlocking brick', 'Title') expect($summary).to.contain( 'show you how to make a brick using the injection machine', diff --git a/packages/cypress/src/integration/questions/discussions.spec.ts b/packages/cypress/src/integration/questions/discussions.spec.ts index 44759c46df..1b3be485a5 100644 --- a/packages/cypress/src/integration/questions/discussions.spec.ts +++ b/packages/cypress/src/integration/questions/discussions.spec.ts @@ -36,11 +36,13 @@ describe('[Questions.Discussions]', () => { cy.addComment(newComment) cy.contains(`${discussion.comments.length + 1} comments`) cy.contains(newComment) + cy.contains('less than a minute ago') cy.step('Can edit their comment') cy.editDiscussionItem('CommentItem', updatedNewComment) cy.contains(updatedNewComment) cy.contains(newComment).should('not.exist') + cy.contains('Edited less than a minute ago') cy.step('Can add reply') cy.addReply(newReply) diff --git a/src/pages/Research/Content/ResearchArticle.test.tsx b/src/pages/Research/Content/ResearchArticle.test.tsx index c8949fc4d3..b437c93cf6 100644 --- a/src/pages/Research/Content/ResearchArticle.test.tsx +++ b/src/pages/Research/Content/ResearchArticle.test.tsx @@ -9,6 +9,7 @@ import { import { ThemeProvider } from '@emotion/react' import { faker } from '@faker-js/faker' import { act, render, waitFor, within } from '@testing-library/react' +import { formatDistanceToNow } from 'date-fns' import { Provider } from 'mobx-react' import { ResearchUpdateStatus, UserRole } from 'oa-shared' import { useResearchStore } from 'src/stores/Research/research.store' @@ -19,7 +20,6 @@ import { } from 'src/test/factories/ResearchItem' import { FactoryUser } from 'src/test/factories/User' import { testingThemeStyles } from 'src/test/utils/themeUtils' -import { formatDate } from 'src/utils/date' import { describe, expect, it, vi } from 'vitest' import ResearchArticle from './ResearchArticle' @@ -263,11 +263,9 @@ describe('Research Article', () => { it('does not show edit timestamp, when create displays the same value', async () => { const created = faker.date.past() - const modified = new Date(created) - modified.setHours(15) const update = FactoryResearchItemUpdate({ _created: created.toString(), - _modified: modified.toString(), + _modified: created.toString(), title: 'A title', description: 'A description', }) @@ -280,22 +278,32 @@ describe('Research Article', () => { updates: [update], }), }) - // Act const wrapper = getWrapper() // Assert await waitFor(() => { expect(() => - wrapper.getAllByText(`edited ${formatDate(modified)}`), + wrapper.getAllByText( + `${formatDistanceToNow(update._modified, { addSuffix: true })}`, + ), + ).toThrow() + }) + await waitFor(() => { + expect(() => + wrapper.getAllByText((content) => content.includes('edited')), ).toThrow() + expect(() => + wrapper.getAllByText((content) => content.includes('created')), + ).not.toThrow() }) }) it('does show both created and edit timestamp, when different', async () => { - const modified = faker.date.future() + const modified = faker.date.past({ years: 1 }) + const created = faker.date.past({ years: 2 }) const update = FactoryResearchItemUpdate({ - _created: faker.date.past().toString(), + _created: created.toString(), status: ResearchUpdateStatus.PUBLISHED, _modified: modified.toString(), title: 'A title', @@ -314,11 +322,23 @@ describe('Research Article', () => { // Act const wrapper = getWrapper() - // Assert await waitFor(() => { expect(() => - wrapper.getAllByText(`edited ${formatDate(modified)}`), + wrapper.getAllByText((content) => content.includes('created')), + ).not.toThrow() + expect(() => + wrapper.getAllByText( + `${formatDistanceToNow(created, { addSuffix: true })}`, + ), + ).not.toThrow() + expect(() => + wrapper.getAllByText((content) => content.includes('edited')), + ).not.toThrow() + expect(() => + wrapper.getAllByText( + `${formatDistanceToNow(modified, { addSuffix: true })}`, + ), ).not.toThrow() }) }) diff --git a/src/pages/Research/Content/ResearchListItem.tsx b/src/pages/Research/Content/ResearchListItem.tsx index 3f4441d5cd..06ac728600 100644 --- a/src/pages/Research/Content/ResearchListItem.tsx +++ b/src/pages/Research/Content/ResearchListItem.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { Category, + DisplayDate, Icon, IconCountWithTooltip, InternalLink, @@ -14,7 +15,6 @@ import { } from 'oa-shared' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { cdnImageUrl } from 'src/utils/cdnImageUrl' -import { formatDate } from 'src/utils/date' import { Box, Card, Flex, Grid, Heading, Image, Text } from 'theme-ui' import defaultResearchThumbnail from '../../../assets/images/default-research-thumbnail.jpg' @@ -182,7 +182,7 @@ const ResearchListItem = ({ item }: IProps) => { )} {/* Hide this on mobile, show on tablet & above. */} - {modifiedDate && ( + {modifiedDate !== '' && ( { ) } -const getItemDate = (item: IResearch.Item, variant: string): string => { +const getItemDate = (item: IResearch.Item, variant: string) => { try { - const contentModifiedDate = formatDate( - new Date(item._contentModifiedTimestamp), + const contentModifiedDate = ( + ) - const creationDate = formatDate(new Date(item._created)) + const creationDate = - if (contentModifiedDate !== creationDate) { - return variant === 'long' - ? `Updated ${contentModifiedDate}` - : contentModifiedDate + if (item._contentModifiedTimestamp !== item._created) { + return variant === 'long' ? ( + <>Updated {contentModifiedDate} + ) : ( + contentModifiedDate + ) } else { - return variant === 'long' ? `Created ${creationDate}` : creationDate + return variant === 'long' ? <>Created {creationDate} : creationDate } } catch (err) { return '' diff --git a/src/pages/Research/Content/ResearchUpdate.tsx b/src/pages/Research/Content/ResearchUpdate.tsx index 266a7f2f34..7ad699f007 100644 --- a/src/pages/Research/Content/ResearchUpdate.tsx +++ b/src/pages/Research/Content/ResearchUpdate.tsx @@ -1,6 +1,7 @@ import { Link, useNavigate } from 'react-router-dom' import { Button, + DisplayDate, DownloadCounter, DownloadFileFromLink, DownloadStaticFile, @@ -13,7 +14,6 @@ import { import { useContributorsData } from 'src/common/hooks/contributorsData' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { useResearchStore } from 'src/stores/Research/research.store' -import { formatDate } from 'src/utils/date' import { formatImagesForGallery } from 'src/utils/formatImageListForGallery' import { Box, Card, Flex, Heading, Text } from 'theme-ui' @@ -51,8 +51,6 @@ const ResearchUpdate = (props: IProps) => { const loggedInUser = useCommonStores().stores.userStore.activeUser const contributors = useContributorsData(collaborators || []) - const formattedCreateDatestamp = formatDate(new Date(_created)) - const formattedModifiedDatestamp = formatDate(new Date(_modified)) const research = researchStore.activeResearchItem const handleDownloadClick = async () => { @@ -154,18 +152,17 @@ const ResearchUpdate = (props: IProps) => { textAlign: ['left', 'right', 'right'], }} > - {'created ' + formattedCreateDatestamp} + created - {formattedCreateDatestamp !== - formattedModifiedDatestamp && ( + {_created !== _modified && ( - {'edited ' + formattedModifiedDatestamp} + edited )} diff --git a/src/pages/common/ContentAuthorTimestamp/ContentAuthorTimestamp.tsx b/src/pages/common/ContentAuthorTimestamp/ContentAuthorTimestamp.tsx index df6f08a13f..0066e37834 100644 --- a/src/pages/common/ContentAuthorTimestamp/ContentAuthorTimestamp.tsx +++ b/src/pages/common/ContentAuthorTimestamp/ContentAuthorTimestamp.tsx @@ -1,4 +1,4 @@ -import { formatDate } from 'src/utils/date' +import { DisplayDate } from 'oa-components' import { Box, Text } from 'theme-ui' import { UserNameTag } from '../UserNameTag/UserNameTag' @@ -18,13 +18,6 @@ export const ContentAuthorTimestamp = ({ modified, action, }: ContentAuthorTimestampProps) => { - const contentModifiedDate = formatDate(new Date(modified)) - const creationDate = formatDate(new Date(created)) - const modifiedDateText = - contentModifiedDate !== creationDate - ? `Last update on ${contentModifiedDate}` - : '' - return ( ) diff --git a/src/pages/common/UserNameTag/UserNameTag.tsx b/src/pages/common/UserNameTag/UserNameTag.tsx index 79de12429b..a82cc2fbc0 100644 --- a/src/pages/common/UserNameTag/UserNameTag.tsx +++ b/src/pages/common/UserNameTag/UserNameTag.tsx @@ -1,6 +1,5 @@ -import { Username } from 'oa-components' +import { DisplayDate, Username } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { formatDate } from 'src/utils/date' import { Flex, Text } from 'theme-ui' interface UserNameTagProps { @@ -18,7 +17,6 @@ export const UserNameTag = ({ }: UserNameTagProps) => { const { aggregationsStore } = useCommonStores().stores - const dateText = `| ${action} on ${formatDate(new Date(created))}` const isVerified = aggregationsStore.isVerified(userName) return ( @@ -40,7 +38,7 @@ export const UserNameTag = ({ marginBottom: 2, }} > - {dateText} + | {action} diff --git a/src/utils/date.ts b/src/utils/date.ts deleted file mode 100644 index 2b7f089c88..0000000000 --- a/src/utils/date.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { format } from 'date-fns' - -export const formatDate = (date: Date) => { - return format(date, 'dd-MM-yyyy') -} - -export const formatDateTime = (date: Date) => { - return format(date, 'dd-MM-yyyy HH:mm') -}