From 62996d6585fe72ecc59723dfc1bec330380d3e25 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 17 Jun 2020 12:24:32 +0200 Subject: [PATCH] align calculating and formatting of dates between components --- ...teFromNow.stories.tsx => Date.stories.tsx} | 48 ++++--- .../src/{dates.ts => DateElement.ts} | 15 +-- scm-ui/ui-components/src/DateFromNow.tsx | 104 +++------------ scm-ui/ui-components/src/DateShort.tsx | 21 +-- .../src/repos/annotate/Annotate.tsx | 2 +- .../src/repos/annotate/AnnotateLine.tsx | 2 +- .../src/repos/annotate/Popover.tsx | 2 +- ...omNow.test.ts => useDateFormatter.test.ts} | 2 +- scm-ui/ui-components/src/useDateFormatter.ts | 126 ++++++++++++++++++ 9 files changed, 190 insertions(+), 132 deletions(-) rename scm-ui/ui-components/src/{DateFromNow.stories.tsx => Date.stories.tsx} (68%) rename scm-ui/ui-components/src/{dates.ts => DateElement.ts} (78%) rename scm-ui/ui-components/src/{DateFromNow.test.ts => useDateFormatter.test.ts} (96%) create mode 100644 scm-ui/ui-components/src/useDateFormatter.ts diff --git a/scm-ui/ui-components/src/DateFromNow.stories.tsx b/scm-ui/ui-components/src/Date.stories.tsx similarity index 68% rename from scm-ui/ui-components/src/DateFromNow.stories.tsx rename to scm-ui/ui-components/src/Date.stories.tsx index 8259bf6240..4d2fe16fbe 100644 --- a/scm-ui/ui-components/src/DateFromNow.stories.tsx +++ b/scm-ui/ui-components/src/Date.stories.tsx @@ -24,25 +24,41 @@ import React from "react"; import DateFromNow from "./DateFromNow"; import { storiesOf } from "@storybook/react"; +import DateShort from "./DateShort"; +import styled from "styled-components"; const baseProps = { timeZone: "Europe/Berlin", baseDate: "2019-10-12T13:56:42+02:00" }; -storiesOf("DateFromNow", module).add("Default", () => ( -
-

- -

-

- -

-

- -

-

- -

-
-)); +const dates = [ + "2009-06-30T18:30:00+02:00", + "2019-06-30T18:30:00+02:00", + "2019-10-12T13:56:40+02:00", + "2019-10-11T13:56:40+02:00" +]; + +const Wrapper = styled.div` + padding: 2rem; +`; + +storiesOf("Date", module) + .add("Date from now", () => ( + + {dates.map(d => ( +

+ +

+ ))} +
+ )) + .add("Short", () => ( + + {dates.map(d => ( +

+ +

+ ))} +
+ )); diff --git a/scm-ui/ui-components/src/dates.ts b/scm-ui/ui-components/src/DateElement.ts similarity index 78% rename from scm-ui/ui-components/src/dates.ts rename to scm-ui/ui-components/src/DateElement.ts index f79fe25e63..5c4dd25afa 100644 --- a/scm-ui/ui-components/src/dates.ts +++ b/scm-ui/ui-components/src/DateElement.ts @@ -22,21 +22,10 @@ * SOFTWARE. */ import styled from "styled-components"; -import { parseISO } from "date-fns"; -export type DateInput = Date | string; - -export const ShortDateFormat = "yyyy-MM-dd"; -export const FullDateFormat = "yyyy-MM-dd HH:mm:ss"; - -export const DateElement = styled.time` +const DateElement = styled.time` border-bottom: 1px dotted rgba(219, 219, 219); cursor: help; `; -export const toDate = (value: DateInput): Date => { - if (value instanceof Date) { - return value; - } - return parseISO(value); -}; +export default DateElement; diff --git a/scm-ui/ui-components/src/DateFromNow.tsx b/scm-ui/ui-components/src/DateFromNow.tsx index b01cfcdc7b..8b5a6c0a37 100644 --- a/scm-ui/ui-components/src/DateFromNow.tsx +++ b/scm-ui/ui-components/src/DateFromNow.tsx @@ -21,100 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; -import { withTranslation, WithTranslation } from "react-i18next"; -import { formatDistance, format, Locale } from "date-fns"; -import { enUS, de, es } from "date-fns/locale"; -import { DateInput, DateElement, FullDateFormat, toDate } from "./dates"; -type LocaleMap = { - [key: string]: Locale; -}; +import React, { FC } from "react"; +import useDateFormatter, { DateProps } from "./useDateFormatter"; +import DateElement from "./DateElement"; -export const supportedLocales: LocaleMap = { - enUS, - en: enUS, - de, - es -}; - -type Props = WithTranslation & { - date?: DateInput; - timeZone?: string; - - /** - * baseDate is the date from which the distance is calculated, - * default is the current time (new Date()). This property - * is required to keep snapshots tests green over the time on - * ci server. - */ - baseDate?: DateInput; +type Props = DateProps & { className?: string; }; -type Options = { - addSuffix: boolean; - locale: Locale; - timeZone?: string; -}; - -export const chooseLocale = (language: string, languages?: string[]) => { - for (const lng of languages || []) { - const locale = supportedLocales[lng]; - if (locale) { - return locale; - } - } - - const locale = supportedLocales[language]; - if (locale) { - return locale; - } - - return enUS; -}; - -class DateFromNow extends React.Component { - getLocale = (): Locale => { - const { i18n } = this.props; - return chooseLocale(i18n.language, i18n.languages); - }; - - createOptions = () => { - const { timeZone } = this.props; - const options: Options = { - addSuffix: true, - locale: this.getLocale() - }; - if (timeZone) { - options.timeZone = timeZone; - } - return options; - }; - - getBaseDate = () => { - const { baseDate } = this.props; - if (baseDate) { - return toDate(baseDate); - } - return new Date(); - }; - - render() { - const { date, className } = this.props; - if (date) { - const isoDate = toDate(date); - const options = this.createOptions(); - const distance = formatDistance(isoDate, this.getBaseDate(), options); - const formatted = format(isoDate, FullDateFormat, options); - return ( - - {distance} - - ); - } +const DateFromNow: FC = ({ className, ...dateProps }) => { + const formatter = useDateFormatter(dateProps); + if (!formatter) { return null; } -} -export default withTranslation()(DateFromNow); + return ( + + {formatter.formatDistance()} + + ); +}; + +export default DateFromNow; diff --git a/scm-ui/ui-components/src/DateShort.tsx b/scm-ui/ui-components/src/DateShort.tsx index 86510177ad..d0da04cb87 100644 --- a/scm-ui/ui-components/src/DateShort.tsx +++ b/scm-ui/ui-components/src/DateShort.tsx @@ -21,23 +21,24 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC } from "react"; -import { format } from "date-fns"; -import { toDate, ShortDateFormat, FullDateFormat, DateElement } from "./dates"; -type Props = { - value?: Date | string; +import React, { FC } from "react"; +import useDateFormatter, { DateProps } from "./useDateFormatter"; +import DateElement from "./DateElement"; + +type Props = DateProps & { className?: string; }; -const DateShort: FC = ({ value, className }) => { - if (!value) { +const DateShort: FC = ({ className, ...dateProps }) => { + const formatter = useDateFormatter(dateProps); + if (!formatter) { return null; } - const date = toDate(value); + return ( - - {format(date, ShortDateFormat)} + + {formatter.formatShort()} ); }; diff --git a/scm-ui/ui-components/src/repos/annotate/Annotate.tsx b/scm-ui/ui-components/src/repos/annotate/Annotate.tsx index ab1cc69696..9ee38a7733 100644 --- a/scm-ui/ui-components/src/repos/annotate/Annotate.tsx +++ b/scm-ui/ui-components/src/repos/annotate/Annotate.tsx @@ -28,7 +28,7 @@ import { Repository, AnnotatedSource, AnnotatedLine } from "@scm-manager/ui-type // @ts-ignore import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter"; import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; -import { DateInput } from "../../dates"; +import { DateInput } from "../../useDateFormatter"; import Popover from "./Popover"; import AnnotateLine from "./AnnotateLine"; import { Action } from "./actions"; diff --git a/scm-ui/ui-components/src/repos/annotate/AnnotateLine.tsx b/scm-ui/ui-components/src/repos/annotate/AnnotateLine.tsx index cb6b2ffb86..7d8401aa70 100644 --- a/scm-ui/ui-components/src/repos/annotate/AnnotateLine.tsx +++ b/scm-ui/ui-components/src/repos/annotate/AnnotateLine.tsx @@ -134,7 +134,7 @@ const AnnotateLine: FC = ({ annotation, showAnnotation, dispatch, nr, chi {annotation.author.name} {" "} - + {" "} {nr} {children} diff --git a/scm-ui/ui-components/src/repos/annotate/Popover.tsx b/scm-ui/ui-components/src/repos/annotate/Popover.tsx index b4edb785cb..1c4590af04 100644 --- a/scm-ui/ui-components/src/repos/annotate/Popover.tsx +++ b/scm-ui/ui-components/src/repos/annotate/Popover.tsx @@ -27,7 +27,7 @@ import styled from "styled-components"; import { Link } from "react-router-dom"; import DateFromNow from "../../DateFromNow"; import { SingleContributor } from "../changesets"; -import { DateInput } from "../../dates"; +import { DateInput } from "../../useDateFormatter"; import { Repository, AnnotatedLine } from "@scm-manager/ui-types"; import AuthorImage from "./AuthorImage"; import { Action } from "./actions"; diff --git a/scm-ui/ui-components/src/DateFromNow.test.ts b/scm-ui/ui-components/src/useDateFormatter.test.ts similarity index 96% rename from scm-ui/ui-components/src/DateFromNow.test.ts rename to scm-ui/ui-components/src/useDateFormatter.test.ts index 19acc08fda..4f0238332c 100644 --- a/scm-ui/ui-components/src/DateFromNow.test.ts +++ b/scm-ui/ui-components/src/useDateFormatter.test.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { chooseLocale, supportedLocales } from "./DateFromNow"; +import { chooseLocale, supportedLocales } from "./useDateFormatter"; describe("test choose locale", () => { it("should choose de", () => { diff --git a/scm-ui/ui-components/src/useDateFormatter.ts b/scm-ui/ui-components/src/useDateFormatter.ts new file mode 100644 index 0000000000..798b2d5d39 --- /dev/null +++ b/scm-ui/ui-components/src/useDateFormatter.ts @@ -0,0 +1,126 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { useTranslation } from "react-i18next"; +import { enUS, de, es } from "date-fns/locale"; +import { formatDistance, format, Locale, parseISO } from "date-fns"; + +type LocaleMap = { + [key: string]: Locale; +}; + +export const supportedLocales: LocaleMap = { + enUS, + en: enUS, + de, + es +}; + +type Options = { + addSuffix: boolean; + locale: Locale; + timeZone?: string; +}; + +export const chooseLocale = (language: string, languages?: string[]) => { + for (const lng of languages || []) { + const locale = supportedLocales[lng]; + if (locale) { + return locale; + } + } + + const locale = supportedLocales[language]; + if (locale) { + return locale; + } + + return enUS; +}; + +export type DateInput = Date | string; + +export type DateProps = { + date?: DateInput; + timeZone?: string; + + /** + * baseDate is the date from which the distance is calculated, + * default is the current time (new Date()). This property + * is required to keep snapshots tests green over the time on + * ci server. + */ + baseDate?: DateInput; +}; + +const createOptions = (locale: Locale, timeZone?: string) => { + const options: Options = { + addSuffix: true, + locale + }; + if (timeZone) { + options.timeZone = timeZone; + } + return options; +}; + +const createBaseDate = (baseDate?: DateInput) => { + if (baseDate) { + return toDate(baseDate); + } + return new Date(); +}; + +const toDate = (value: DateInput): Date => { + if (value instanceof Date) { + return value; + } + return parseISO(value); +}; + +const useDateFormatter = ({ date, baseDate, timeZone }: DateProps) => { + const { i18n } = useTranslation(); + if (!date) { + return null; + } + + const isoDate = toDate(date); + const base = createBaseDate(baseDate); + + const locale = chooseLocale(i18n.language, i18n.languages); + const options = createOptions(locale, timeZone); + return { + formatShort() { + return format(isoDate, "yyyy-MM-dd", options); + }, + formatFull() { + return format(isoDate, "yyyy-MM-dd HH:mm:ss", options); + }, + formatDistance() { + return formatDistance(isoDate, base, options); + } + }; +}; + +export default useDateFormatter;