diff --git a/src/components/DocPage/DocPage.tsx b/src/components/DocPage/DocPage.tsx index 8b06ef3b..b2615fc3 100644 --- a/src/components/DocPage/DocPage.tsx +++ b/src/components/DocPage/DocPage.tsx @@ -1,4 +1,4 @@ -import React, {ReactPortal} from 'react'; +import React, {Fragment, ReactPortal} from 'react'; import {Link} from '@gravity-ui/icons'; import block from 'bem-cn-lite'; @@ -28,6 +28,7 @@ import {HTML} from '../HTML'; import {MiniToc} from '../MiniToc'; import {SearchBar, withHighlightedSearchWords} from '../SearchBar'; import {TocNavPanel} from '../TocNavPanel'; +import UpdatedAtDate from '../UpdatedAtDate/UpdatedAtDate'; import './DocPage.scss'; @@ -382,18 +383,30 @@ class DocPage extends React.Component { return null; } + const updatedAt = this.renderUpdatedAt(meta?.updatedAt); const author = this.renderAuthor(!meta?.contributors?.length); const contributors = this.renderContributors(); - const separator = author && contributors &&
{','}
; - return (
- {author} {separator} {contributors} + {[updatedAt, author, contributors].filter(Boolean).map((element, idx, arr) => ( + + {element} + {arr.length - 1 !== idx &&
{','}
} +
+ ))}
); } + private renderUpdatedAt(updatedAt?: string) { + if (!updatedAt) { + return null; + } + + return ; + } + private renderAuthor(onlyAuthor: boolean) { const {meta} = this.props; diff --git a/src/components/UpdatedAtDate/UpdatedAtDate.scss b/src/components/UpdatedAtDate/UpdatedAtDate.scss new file mode 100644 index 00000000..d7667998 --- /dev/null +++ b/src/components/UpdatedAtDate/UpdatedAtDate.scss @@ -0,0 +1,10 @@ +@import '../../styles/mixins'; + +.updated-at-date { + display: flex; + margin: 0 0 32px; + + &__title { + @include contributors-text(); + } +} diff --git a/src/components/UpdatedAtDate/UpdatedAtDate.tsx b/src/components/UpdatedAtDate/UpdatedAtDate.tsx new file mode 100644 index 00000000..4dee7657 --- /dev/null +++ b/src/components/UpdatedAtDate/UpdatedAtDate.tsx @@ -0,0 +1,36 @@ +import React, {memo, useMemo} from 'react'; + +import block from 'bem-cn-lite'; + +import {configure, getConfig} from '../../config'; +import {useTranslation} from '../../hooks'; +import {format} from '../../utils/date'; + +import './UpdatedAtDate.scss'; + +const b = block('updated-at-date'); + +export interface UpdatedAtDateProps { + updatedAt: string; + translationName?: string; +} + +const UpdatedAtDate: React.FC = (props) => { + const {updatedAt, translationName = 'updated-at-date'} = props; + const {t} = useTranslation(translationName); + + const updatedAtFormatted = useMemo(() => { + configure(); + const config = getConfig(); + return format(updatedAt, 'longDateTime', config.localeCode); + }, [updatedAt]); + + return ( +
+
{t('title')}
+ {updatedAtFormatted} +
+ ); +}; + +export default memo(UpdatedAtDate); diff --git a/src/i18n/en.json b/src/i18n/en.json index 336defac..b9271bab 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -113,5 +113,8 @@ "paginator": { "next": "Next page", "prev": "Previous page" + }, + "updated-at-date": { + "label-updated-at": "Updated at" } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 874fff15..9f3a5c93 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -113,5 +113,8 @@ "paginator": { "next": "Следующая страница", "prev": "Предыдущая страница" + }, + "updated-at-date": { + "label-updated-at": "Обновлено" } } diff --git a/src/models/index.ts b/src/models/index.ts index 6238747b..632ffc42 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -19,6 +19,7 @@ export enum ControlSizes { export interface Config { lang?: string; loc?: Loc; + localeCode?: string; } export interface DocSettings { @@ -92,6 +93,7 @@ export interface DocMeta { contributors?: Contributor[]; author?: unknown | Contributor; __system?: Record; + updatedAt?: string; } export interface TocData { diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 00000000..5a0a56b9 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,75 @@ +interface DateTimeFormatter { + longDate: Intl.DateTimeFormat; + shortDate: Intl.DateTimeFormat; + longMonthDay: Intl.DateTimeFormat; + shortMonthDay: Intl.DateTimeFormat; + longDateTime: Intl.DateTimeFormat; + shortDateTime: Intl.DateTimeFormat; + shortTime: Intl.DateTimeFormat; + nanoTime: Intl.DateTimeFormat; + year: Intl.DateTimeFormat; +} + +const defaultRegion = 'ru-RU'; + +const dateTimeFormatters: Map = new Map(); + +function getDateTimeFormatter(localeCode: string): DateTimeFormatter { + if (!dateTimeFormatters.has(localeCode)) { + const formatters = { + // December 20, 2012 + longDate: new Intl.DateTimeFormat(localeCode, { + year: 'numeric', + month: 'long', + day: 'numeric', + }), + // 4/30/2021 + shortDate: new Intl.DateTimeFormat(localeCode, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }), + // December 20 + longMonthDay: new Intl.DateTimeFormat(localeCode, {month: 'long', day: 'numeric'}), + // 12/20 + shortMonthDay: new Intl.DateTimeFormat(localeCode, {month: 'numeric', day: 'numeric'}), + // April 27, 2021, 3:03 PM + longDateTime: new Intl.DateTimeFormat(localeCode, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }), + // 4/30/2021, 2:30 PM + shortDateTime: new Intl.DateTimeFormat(localeCode, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }), + // 12:30 + shortTime: new Intl.DateTimeFormat(localeCode, {hour: 'numeric', minute: 'numeric'}), + // 30:58 + nanoTime: new Intl.DateTimeFormat(localeCode, { + minute: 'numeric', + second: 'numeric', + }), + // 2023 + year: new Intl.DateTimeFormat(localeCode, { + year: 'numeric', + }), + }; + dateTimeFormatters.set(localeCode, formatters); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- formatter will be always inserted above + return dateTimeFormatters.get(localeCode)!; +} + +export const format = ( + date: string | number, + formatCode: keyof DateTimeFormatter, + localeCode = defaultRegion, +) => getDateTimeFormatter(localeCode)[formatCode].format(new Date(date));